MDL-42023 assign: Edit PDF plugin - Damyon's contributions
authorDamyon Wiese <damyon@moodle.com>
Fri, 27 Sep 2013 06:51:22 +0000 (14:51 +0800)
committerDamyon Wiese <damyon@moodle.com>
Thu, 3 Oct 2013 01:30:32 +0000 (09:30 +0800)
58 files changed:
lib/pluginlib.php
lib/thirdpartylibs.xml
mod/assign/feedback/editpdf/ajax.php [new file with mode: 0644]
mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php [new file with mode: 0644]
mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/annotation.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/comment.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/comments_quick_list.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/document_services.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/page_editor.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/pdf.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/renderer.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/widget.php [new file with mode: 0644]
mod/assign/feedback/editpdf/db/install.xml [new file with mode: 0755]
mod/assign/feedback/editpdf/fpdi/ASL20.txt [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85_FPDI.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/filters/FilterLZW.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/filters/FilterLZW_FPDI.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/fpdf_tpl.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/fpdi.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/fpdi2tcpdf_bridge.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/fpdi_pdf_parser.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/pdf_context.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/pdf_parser.php [new file with mode: 0644]
mod/assign/feedback/editpdf/fpdi/readme_moodle.txt [new file with mode: 0644]
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php [new file with mode: 0644]
mod/assign/feedback/editpdf/lib.php [new file with mode: 0644]
mod/assign/feedback/editpdf/locallib.php [new file with mode: 0644]
mod/assign/feedback/editpdf/settings.php [new file with mode: 0644]
mod/assign/feedback/editpdf/styles.css [new file with mode: 0644]
mod/assign/feedback/editpdf/testgs.php [new file with mode: 0644]
mod/assign/feedback/editpdf/tests/editpdf_test.php [new file with mode: 0644]
mod/assign/feedback/editpdf/tests/fixtures/submission.pdf [new file with mode: 0644]
mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/build.json [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/annotation.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/annotationhighlight.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/annotationstamp.js
mod/assign/feedback/editpdf/yui/src/editor/js/colourpicker.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/commentmenu.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/commentsearch.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/drawable.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/dropdown.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/edit.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/point.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/quickcomment.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/quickcommentlist.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/js/rect.js [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/src/editor/meta/editor.json [new file with mode: 0644]
mod/assign/gradingtable.php
mod/assign/locallib.php

index 8a82092..742a0d9 100644 (file)
@@ -869,7 +869,7 @@ class plugin_manager {
             ),
 
             'assignfeedback' => array(
-                'comments', 'file', 'offline'
+                'comments', 'file', 'offline', 'editpdf'
             ),
 
             'atto' => array(
index ab36f83..5730b7b 100644 (file)
     <version>1.2</version>
     <licenseversion></licenseversion>
   </library>
+  <library>
+    <location>mod/assign/feedback/editpdf/fpdi</location>
+    <name>FPDI</name>
+    <license>Apache</license>
+    <version>1.4.4</version>
+    <licenseversion>2.0</licenseversion>
+  </library>
 </libraries>
diff --git a/mod/assign/feedback/editpdf/ajax.php b/mod/assign/feedback/editpdf/ajax.php
new file mode 100644 (file)
index 0000000..889b55d
--- /dev/null
@@ -0,0 +1,179 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Process ajax requests
+ *
+ * @package assignfeedback_editpdf
+ * @copyright  2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use \assignfeedback_editpdf\document_services;
+use \assignfeedback_editpdf\page_editor;
+use \assignfeedback_editpdf\comments_quick_list;
+
+if (!defined('AJAX_SCRIPT')) {
+    define('AJAX_SCRIPT', true);
+}
+
+require_once('../../../../config.php');
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+require_sesskey();
+
+$action = optional_param('action', '', PARAM_ALPHANUM);
+$assignmentid = required_param('assignmentid', PARAM_INT);
+$userid = required_param('userid', PARAM_INT);
+$attemptnumber = required_param('attemptnumber', PARAM_INT);
+
+$cm = \get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST);
+$context = \context_module::instance($cm->id);
+
+$assignment = new \assign($context, null, null);
+
+if (!$assignment->can_view_submission($userid)) {
+    print_error('nopermission');
+}
+
+if ($action == 'loadallpages') {
+    $draft = true;
+    if (!has_capability('mod/assign:grade', $context)) {
+        $draft = false;
+        require_capability('mod/assign:submit', $context);
+    }
+
+    $pages = document_services::get_page_images_for_attempt($assignment,
+                                                            $userid,
+                                                            $attemptnumber);
+
+    $response = new stdClass();
+    $response->pagecount = count($pages);
+    $response->pages = array();
+
+    $grade = $assignment->get_user_grade($userid, true);
+
+    foreach ($pages as $id => $pagefile) {
+        $index = count($response->pages);
+        $page = new stdClass();
+        $comments = page_editor::get_comments($grade->id, $index, $draft);
+        $page->url = moodle_url::make_pluginfile_url($context->id,
+                                                     'assignfeedback_editpdf',
+                                                     document_services::PAGE_IMAGE_FILEAREA,
+                                                     $grade->id,
+                                                     '/',
+                                                     $pagefile->get_filename())->out();
+        $page->comments = $comments;
+        $annotations = page_editor::get_annotations($grade->id, $index, $draft);
+        $page->annotations = $annotations;
+        array_push($response->pages, $page);
+    }
+
+    echo json_encode($response);
+    die();
+} else if ($action == 'savepage') {
+    require_capability('mod/assign:grade', $context);
+
+    $response = new stdClass();
+    $response->errors = array();
+
+    $grade = $assignment->get_user_grade($userid, true);
+
+    $pagejson = required_param('page', PARAM_RAW);
+    $page = json_decode($pagejson);
+    $index = required_param('index', PARAM_INT);
+
+    $added = page_editor::set_comments($grade->id, $index, $page->comments);
+    if ($added != count($page->comments)) {
+        array_push($response->errors, get_string('couldnotsavepage', 'assignfeedback_editpdf', $index+1));
+    }
+    $added = page_editor::set_annotations($grade->id, $index, $page->annotations);
+    if ($added != count($page->annotations)) {
+        array_push($response->errors, get_string('couldnotsavepage', 'assignfeedback_editpdf', $index+1));
+    }
+    echo json_encode($response);
+    die();
+
+} else if ($action == 'generatepdf') {
+
+    require_capability('mod/assign:grade', $context);
+    $response = new stdClass();
+    $grade = $assignment->get_user_grade($userid, true);
+    $file = document_services::generate_feedback_document($assignment, $userid, $attemptnumber);
+
+    $response->url = '';
+    if ($file) {
+        $url = moodle_url::make_pluginfile_url($assignment->get_context()->id,
+                                               'assignfeedback_editpdf',
+                                               document_services::FINAL_PDF_FILEAREA,
+                                               $grade->id,
+                                               '/',
+                                               $file->get_filename(),
+                                               false);
+        $response->url = $url->out(true);
+        $response->filename = $file->get_filename();
+    }
+
+    echo json_encode($response);
+    die();
+} else if ($action == 'loadquicklist') {
+    require_capability('mod/assign:grade', $context);
+
+    $result = comments_quick_list::get_comments();
+
+    echo json_encode($result);
+    die();
+
+} else if ($action == 'addtoquicklist') {
+    require_capability('mod/assign:grade', $context);
+
+    $comment = required_param('commenttext', PARAM_RAW);
+    $width = required_param('width', PARAM_INT);
+    $colour = required_param('colour', PARAM_ALPHA);
+
+    $result = comments_quick_list::add_comment($comment, $width, $colour);
+
+    echo json_encode($result);
+    die();
+} else if ($action == 'revertchanges') {
+    require_capability('mod/assign:grade', $context);
+
+    $grade = $assignment->get_user_grade($userid, true);
+
+    $result = page_editor::revert_drafts($gradeid);
+
+    echo json_encode($result);
+    die();
+} else if ($action == 'removefromquicklist') {
+    require_capability('mod/assign:grade', $context);
+
+    $commentid = required_param('commentid', PARAM_INT);
+
+    $result = comments_quick_list::remove_comment($commentid);
+
+    echo json_encode($result);
+    die();
+} else if ($action == 'deletefeedbackdocument') {
+    require_capability('mod/assign:grade', $context);
+
+    $grade = $assignment->get_user_grade($userid, true);
+    $result = document_services::delete_feedback_document($assignment, $userid, $attemptnumber);
+
+    $result = $result && page_editor::unrelease_drafts($grade->id);
+    echo json_encode($result);
+    die();
+}
+
diff --git a/mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php b/mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php
new file mode 100644 (file)
index 0000000..546b876
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the backup code for the feedback_editpdf plugin.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Provides the information to backup feedback pdf annotations.
+ *
+ * This just adds its fileareas to the annotations and the comments and annotation data.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_assignfeedback_editpdf_subplugin extends backup_subplugin {
+
+    /**
+     * Returns the subplugin information to attach to feedback element
+     * @return backup_subplugin_element
+     */
+    protected function define_grade_subplugin_structure() {
+
+        // Create XML elements.
+        $subplugin = $this->get_subplugin_element();
+        $subpluginwrapper = new backup_nested_element($this->get_recommended_name());
+        $subpluginelementfiles = new backup_nested_element('feedback_editpdf_files', null, array('gradeid'));
+        $subpluginelementannotations = new backup_nested_element('feedback_editpdf_annotations');
+        $subpluginelementannotation = new backup_nested_element('annotation', null, array('gradeid', 'pageno', 'type', 'x', 'y', 'endx', 'endy', 'colour', 'path', 'draft'));
+        $subpluginelementcomments = new backup_nested_element('feedback_editpdf_comments');
+        $subpluginelementcomment = new backup_nested_element('comment', null, array('gradeid', 'pageno', 'x', 'y', 'width', 'rawtext', 'colour', 'draft'));
+
+        // Connect XML elements into the tree.
+        $subplugin->add_child($subpluginwrapper);
+        $subpluginelementannotations->add_child($subpluginelementannotation);
+        $subpluginelementcomments->add_child($subpluginelementcomment);
+        $subpluginwrapper->add_child($subpluginelementfiles);
+        $subpluginwrapper->add_child($subpluginelementannotations);
+        $subpluginwrapper->add_child($subpluginelementcomments);
+
+        // Set source to populate the data.
+        $subpluginelementfiles->set_source_sql('SELECT id AS gradeid from {assign_grades} where id = :gradeid', array('gradeid' => backup::VAR_PARENTID));
+        $subpluginelementannotation->set_source_table('assignfeedback_editpdf_annot', array('gradeid' => backup::VAR_PARENTID));
+        $subpluginelementcomment->set_source_table('assignfeedback_editpdf_cmnt', array('gradeid' => backup::VAR_PARENTID));
+        // We only need to backup the files in the final pdf area - all the others can be regenerated.
+        $subpluginelementfiles->annotate_files('assignfeedback_editpdf', 'download', 'gradeid');
+        $subpluginelementfiles->annotate_files('assignfeedback_editpdf', 'stamps', 'gradeid');
+        return $subplugin;
+    }
+
+}
diff --git a/mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php b/mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php
new file mode 100644 (file)
index 0000000..cb7923e
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the restore code for the feedback_editpdf plugin.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Restore subplugin class.
+ *
+ * Provides the necessary information needed
+ * to restore one assign_feedback subplugin.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_assignfeedback_editpdf_subplugin extends restore_subplugin {
+
+    /**
+     * Returns the paths to be handled by the subplugin at assignment level
+     * @return array
+     */
+    protected function define_grade_subplugin_structure() {
+
+        $paths = array();
+
+        // We used get_recommended_name() so this works.
+        // The files node is a placeholder just containing gradeid so we can restore files once per grade.
+        $elename = $this->get_namefor('files');
+        $elepath = $this->get_pathfor('/feedback_editpdf_files');
+        $paths[] = new restore_path_element($elename, $elepath);
+
+        // Now we have the list of comments and annotations per grade.
+        $elename = $this->get_namefor('comment');
+        $elepath = $this->get_pathfor('/feedback_editpdf_comments/comment');
+        $paths[] = new restore_path_element($elename, $elepath);
+        $elename = $this->get_namefor('annotation');
+        $elepath = $this->get_pathfor('/feedback_editpdf_annotations/annotation');
+        $paths[] = new restore_path_element($elename, $elepath);
+
+        return $paths;
+    }
+
+    /**
+     * Processes one feedback_editpdf_files element
+     * @param mixed $data
+     */
+    public function process_assignfeedback_editpdf_files($data) {
+        $data = (object)$data;
+
+        // In this case the id is the old gradeid which will be mapped.
+        $this->add_related_files('assignfeedback_editpdf', 'download', 'grade', null, $data->gradeid);
+        $this->add_related_files('assignfeedback_editpdf', 'stamps', 'grade', null, $data->gradeid);
+    }
+
+    /**
+     * Processes one feedback_editpdf_annotations/annotation element
+     * @param mixed $data
+     */
+    public function process_assignfeedback_editpdf_annotation($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $oldgradeid = $data->gradeid;
+        // The mapping is set in the restore for the core assign activity
+        // when a grade node is processed.
+        $data->gradeid = $this->get_mappingid('grade', $data->gradeid);
+
+        $DB->insert_record('assignfeedback_editpdf_annot', $data);
+
+    }
+
+    /**
+     * Processes one feedback_editpdf_comments/comment element
+     * @param mixed $data
+     */
+    public function process_assignfeedback_editpdf_comment($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $oldgradeid = $data->gradeid;
+        // The mapping is set in the restore for the core assign activity
+        // when a grade node is processed.
+        $data->gradeid = $this->get_mappingid('grade', $data->gradeid);
+
+        $DB->insert_record('assignfeedback_editpdf_cmnt', $data);
+
+    }
+
+}
diff --git a/mod/assign/feedback/editpdf/classes/annotation.php b/mod/assign/feedback/editpdf/classes/annotation.php
new file mode 100644 (file)
index 0000000..af44aba
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the annotation class for the assignfeedback_editpdf plugin
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf;
+
+/**
+ * This class adds and removes annotations from a page of a response.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class annotation {
+
+    /** @var int unique id for this annotation */
+    public $id = 0;
+
+    /** @var int gradeid for this annotation */
+    public $gradeid = 0;
+
+    /** @var int page number for this annotation */
+    public $pageno = 0;
+
+    /** @var int starting location in pixels. Image resolution is 100 pixels per inch */
+    public $x = 0;
+
+    /** @var int ending location in pixels. Image resolution is 100 pixels per inch */
+    public $endx = 0;
+
+    /** @var int starting location in pixels. Image resolution is 100 pixels per inch */
+    public $y = 0;
+
+    /** @var int ending location in pixels. Image resolution is 100 pixels per inch */
+    public $endy = 0;
+
+    /** @var string path information for drawing the annotation. */
+    public $path = '';
+
+    /** @var string colour - One of red, yellow, green, blue, white */
+    public $colour = 'yellow';
+
+    /** @var string type - One of line, oval, rect, etc */
+    public $type = 'line';
+
+    /**
+     * Convert a compatible stdClass into an instance of this class.
+     * @param stdClass $record
+     */
+    public function __construct(\stdClass $record) {
+        $intcols = array('endx', 'endy', 'x', 'y');
+        foreach ($this as $key => $value) {
+            if (isset($record->$key)) {
+                if (in_array($key, $intcols)) {
+                    $this->$key = intval($record->$key);
+                } else {
+                    $this->$key = $record->$key;
+                }
+            }
+        }
+    }
+}
diff --git a/mod/assign/feedback/editpdf/classes/comment.php b/mod/assign/feedback/editpdf/classes/comment.php
new file mode 100644 (file)
index 0000000..d3dd7a5
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the comment class for the assignfeedback_editpdf plugin
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf;
+
+/**
+ * This class represents a comment box on a page of feedback.
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class comment {
+
+    /** @var int unique id for this annotation */
+    public $id = 0;
+
+    /** @var int gradeid for this annotation */
+    public $gradeid = 0;
+
+    /** @var int page number for this annotation */
+    public $pageno = 0;
+
+    /** @var int starting location in pixels. Image resolution is 100 pixels per inch */
+    public $x = 0;
+
+    /** @var int starting location in pixels. Image resolution is 100 pixels per inch */
+    public $y = 0;
+
+    /** @var int width of the comment box */
+    public $width = 120;
+
+    /** @var string The comment text. */
+    public $rawtext = '';
+
+    /** @var string colour - One of red, yellow, green, blue, white */
+    public $colour = 'yellow';
+
+    /**
+     * Convert a compatible stdClass into an instance of a comment.
+     * @param \stdClass $record
+     */
+    public function __construct(\stdClass $record) {
+        $intcols = array('width', 'x', 'y');
+        foreach ($this as $key => $value) {
+            if (isset($record->$key)) {
+                if (in_array($key, $intcols)) {
+                    $this->$key = intval($record->$key);
+                } else {
+                    $this->$key = $record->$key;
+                }
+            }
+        }
+    }
+}
diff --git a/mod/assign/feedback/editpdf/classes/comments_quick_list.php b/mod/assign/feedback/editpdf/classes/comments_quick_list.php
new file mode 100644 (file)
index 0000000..deb8be2
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the functions for managing a users comments quicklist.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf;
+
+/**
+ * This class performs crud operations on a users quicklist comments.
+ *
+ * No capability checks are done - they should be done by the calling class.
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class comments_quick_list {
+
+    /**
+     * Get all comments for the current user.
+     * @return array(comment)
+     */
+    public static function get_comments() {
+        global $DB, $USER;
+
+        $comments = array();
+        $records = $DB->get_records('assignfeedback_editpdf_quick', array('userid'=>$USER->id));
+
+        return $records;
+    }
+
+    /**
+     * Add a comment to the quick list.
+     * @param string $commenttext
+     * @param int $width
+     * @param string $colour
+     * @return stdClass - the comment record (with new id set)
+     */
+    public static function add_comment($commenttext, $width, $colour) {
+        global $DB, $USER;
+
+        $comment = new \stdClass();
+        $comment->userid = $USER->id;
+        $comment->rawtext = $commenttext;
+        $comment->width = $width;
+        $comment->colour = $colour;
+
+        $comment->id = $DB->insert_record('assignfeedback_editpdf_quick', $comment);
+        return $comment;
+    }
+
+    /**
+     * Get a single comment by id.
+     * @param int $commentid
+     * @return comment or false
+     */
+    public static function get_comment($commentid) {
+        global $DB;
+
+        $record = $DB->get_record('assignfeedback_editpdf_quick', array('id'=>$commentid), '*', IGNORE_MISSING);
+        if ($record) {
+            return $record;
+        }
+        return false;
+    }
+
+    /**
+     * Remove a comment from the quick list.
+     * @param int $commentid
+     * @return bool
+     */
+    public static function remove_comment($commentid) {
+        global $DB, $USER;
+        return $DB->delete_records('assignfeedback_editpdf_quick', array('id'=>$commentid, 'userid'=>$USER->id));
+    }
+}
diff --git a/mod/assign/feedback/editpdf/classes/document_services.php b/mod/assign/feedback/editpdf/classes/document_services.php
new file mode 100644 (file)
index 0000000..e956208
--- /dev/null
@@ -0,0 +1,561 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the ingest manager for the assignfeedback_editpdf plugin
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf;
+
+/**
+ * Functions for generating the annotated pdf.
+ *
+ * This class controls the ingest of student submission files to a normalised
+ * PDF 1.4 document with all submission files concatinated together. It also
+ * provides the functions to generate a downloadable pdf with all comments and
+ * annotations embedded.
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class document_services {
+
+    /** File area for generated pdf */
+    const FINAL_PDF_FILEAREA = 'download';
+    /** File area for combined pdf */
+    const COMBINED_PDF_FILEAREA = 'combined';
+    /** File area for page images */
+    const PAGE_IMAGE_FILEAREA = 'pages';
+    /** Filename for combined pdf */
+    const COMBINED_PDF_FILENAME = 'combined.pdf';
+
+    /**
+     * This function will take an int or an assignment instance and
+     * return an assignment instance. It is just for convenience.
+     * @param int|\assign $assignment
+     * @return assign
+     */
+    private static function get_assignment_from_param($assignment) {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+        if (!is_object($assignment)) {
+            $cm = \get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST);
+            $context = \context_module::instance($cm->id);
+
+            $assignment = new \assign($context, null, null);
+        }
+        return $assignment;
+    }
+
+    /**
+     * Get a hash that will be unique and can be used in a path name.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     */
+    private static function hash($assignment, $userid, $attemptnumber) {
+        if (is_object($assignment)) {
+            $assignmentid = $assignment->get_instance()->id;
+        } else {
+            $assignmentid = $assignment;
+        }
+        return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber);
+    }
+
+    /**
+     * This function will search for all files that can be converted
+     * and concatinated into a PDF (1.4) - for any submission plugin
+     * for this students attempt.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return array(stored_file)
+     */
+    public static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) {
+        global $USER, $DB;
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        // Capability checks.
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+
+        $files = array();
+
+        if ($assignment->get_instance()->teamsubmission) {
+            $submission = $assignment->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assignment->get_user_submission($userid, false);
+        }
+        $user = $DB->get_record('user', array('id' => $userid));
+
+        // User has not submitted anything yet.
+        if (!$submission) {
+            return $files;
+        }
+        // Ask each plugin for it's list of files.
+        foreach ($assignment->get_submission_plugins() as $plugin) {
+            if ($plugin->is_enabled() && $plugin->is_visible()) {
+                $pluginfiles = $plugin->get_files($submission, $user);
+                foreach ($pluginfiles as $filename => $file) {
+                    if ($file->get_mimetype() === 'application/pdf') {
+                        $files[$filename] = $file;
+                    }
+                }
+            }
+        }
+        return $files;
+    }
+
+    /**
+     * This function return the combined pdf for all valid submission files.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return stored_file
+     */
+    public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
+
+        global $USER, $DB;
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        // Capability checks.
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        if ($assignment->get_instance()->teamsubmission) {
+            $submission = $assignment->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assignment->get_user_submission($userid, false);
+        }
+
+        $contextid = $assignment->get_context()->id;
+        $component = 'assignfeedback_editpdf';
+        $filearea = self::COMBINED_PDF_FILEAREA;
+        $itemid = $grade->id;
+        $filepath = '/';
+        $filename = self::COMBINED_PDF_FILENAME;
+        $fs = \get_file_storage();
+
+        $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
+        if (!$combinedpdf ||
+                ($submission && ($combinedpdf->get_timemodified() < $submission->timemodified))) {
+            return self::generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
+        }
+        return $combinedpdf;
+    }
+
+    /**
+     * This function will take all of the compatible files for a submission
+     * and combine them into one PDF.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return stored_file
+     */
+    public static function generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
+        global $CFG;
+
+        require_once($CFG->libdir . '/pdflib.php');
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+
+        $files = self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber);
+
+        $pdf = new pdf();
+        if (!$files) {
+            // No valid submission files - create an empty pdf.
+            $pdf->AddPage();
+        } else {
+
+            // Create a mega joined PDF.
+            $compatiblepdfs = array();
+            foreach ($files as $file) {
+                $compatiblepdf = pdf::ensure_pdf_compatible($file);
+                if ($compatiblepdf) {
+                    array_push($compatiblepdfs, $compatiblepdf);
+                }
+            }
+
+            $tmpdir = \make_temp_directory('assignfeedback_editpdf/combined/' . self::hash($assignment, $userid, $attemptnumber));
+            $tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
+
+            @unlink($tmpfile);
+            $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
+            if ($pagecount == 0) {
+                // We at least want a single blank page.
+                $pdf->AddPage();
+                @unlink($tmpfile);
+                $files = false;
+            }
+        }
+
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        $record = new \stdClass();
+
+        $record->contextid = $assignment->get_context()->id;
+        $record->component = 'assignfeedback_editpdf';
+        $record->filearea = self::COMBINED_PDF_FILEAREA;
+        $record->itemid = $grade->id;
+        $record->filepath = '/';
+        $record->filename = self::COMBINED_PDF_FILENAME;
+        $fs = \get_file_storage();
+
+        $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
+
+        if (!$files) {
+            // This was a blank pdf.
+            $content = $pdf->Output(self::COMBINED_PDF_FILENAME, 'S');
+            $file = $fs->create_file_from_string($record, $content);
+        } else {
+            // This was a combined pdf.
+            $file = $fs->create_file_from_pathname($record, $tmpfile);
+            @unlink($tmpfile);
+        }
+
+        return $file;
+    }
+
+    /**
+     * This function will generate and return a list of the page images from a pdf.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return array(stored_file)
+     */
+    public static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) {
+        global $CFG;
+
+        require_once($CFG->libdir . '/pdflib.php');
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+
+        // Need to generate the page images - first get a combined pdf.
+        $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
+        if (!$file) {
+            throw \moodle_exception('Could not generate combined pdf.');
+        }
+
+        $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
+        $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
+        $file->copy_content_to($combined); // Copy the file.
+
+        $pdf = new pdf();
+
+        $pdf->set_image_folder($tmpdir);
+        $pagecount = $pdf->set_pdf($combined);
+
+        $i = 0;
+        $images = array();
+        for ($i = 0; $i < $pagecount; $i++) {
+            $images[$i] = $pdf->get_image($i);
+        }
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+
+        $files = array();
+        $record = new \stdClass();
+        $record->contextid = $assignment->get_context()->id;
+        $record->component = 'assignfeedback_editpdf';
+        $record->filearea = self::PAGE_IMAGE_FILEAREA;
+        $record->itemid = $grade->id;
+        $record->filepath = '/';
+        $fs = \get_file_storage();
+
+        foreach ($images as $index => $image) {
+            $record->filename = basename($image);
+            $files[$index] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
+            @unlink($tmpdir . '/' . $image);
+        }
+        @unlink($combined);
+        @rmdir($tmpdir);
+
+        return $files;
+    }
+
+    /**
+     * This function returns a list of the page images from a pdf.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return array(stored_file)
+     */
+    public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber) {
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+
+        if ($assignment->get_instance()->teamsubmission) {
+            $submission = $assignment->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assignment->get_user_submission($userid, false);
+        }
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+
+        $contextid = $assignment->get_context()->id;
+        $component = 'assignfeedback_editpdf';
+        $filearea = self::PAGE_IMAGE_FILEAREA;
+        $itemid = $grade->id;
+        $filepath = '/';
+
+        $fs = \get_file_storage();
+
+        $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
+
+        if (!empty($files)) {
+            $first = reset($files);
+            if ($first->get_timemodified() < $submission->timemodified) {
+
+                $fs->delete_area_files($contextid, $component, $filearea, $itemid);
+                // Image files are stale - regenerate them.
+                $files = array();
+            } else {
+                return $files;
+            }
+        }
+        return self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
+    }
+
+    /**
+     * This function returns sensible filename for a feedback file.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return string
+     */
+    protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) {
+        global $DB;
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        $groupmode = groups_get_activity_groupmode($assignment->get_course_module());
+        $groupname = '';
+        if ($groupmode) {
+            $groupid = groups_get_activity_group($assignment->get_course_module(), true);
+            $groupname = groups_get_group_name($groupid).'-';
+        }
+        if ($groupname == '-') {
+            $groupname = '';
+        }
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
+
+        if ($assignment->is_blind_marking()) {
+            $prefix = $groupname . get_string('participant', 'assign');
+            $prefix = str_replace('_', ' ', $prefix);
+            $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
+        } else {
+            $prefix = $groupname . fullname($user);
+            $prefix = str_replace('_', ' ', $prefix);
+            $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
+        }
+        $prefix .= $grade->attemptnumber;
+
+        return $prefix . '.pdf';
+    }
+
+    /**
+     * This function takes the combined pdf and embeds all the comments and annotations.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return stored_file
+     */
+    public static function generate_feedback_document($assignment, $userid, $attemptnumber) {
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+        if (!$assignment->can_grade()) {
+            \print_error('nopermission');
+        }
+
+        // Need to generate the page images - first get a combined pdf.
+        $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
+        if (!$file) {
+            throw \moodle_exception('Could not generate combined pdf.');
+        }
+
+        $tmpdir = \make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber));
+        $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
+        $file->copy_content_to($combined); // Copy the file.
+
+        $pdf = new pdf();
+
+        $fs = \get_file_storage();
+        $stamptmpdir = \make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber));
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        // Copy any new stamps to this instance.
+        if ($files = $fs->get_area_files($assignment->get_context()->id,
+                                         'assignfeedback_editpdf',
+                                         'stamps',
+                                         $grade->id,
+                                         "filename",
+                                         false)) {
+            foreach ($files as $file) {
+                $filename = $stamptmpdir . '/' . $file->get_filename();
+                $file->copy_content_to($filename); // Copy the file.
+            }
+        }
+
+        $pagecount = $pdf->set_pdf($combined);
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        page_editor::release_drafts($grade->id);
+
+        for ($i = 0; $i < $pagecount; $i++) {
+            $pdf->copy_page();
+            $comments = page_editor::get_comments($grade->id, $i, false);
+            $annotations = page_editor::get_annotations($grade->id, $i, false);
+
+            foreach ($comments as $comment) {
+                $pdf->add_comment($comment->rawtext,
+                                  $comment->x,
+                                  $comment->y,
+                                  $comment->width,
+                                  $comment->colour);
+            }
+
+            foreach ($annotations as $annotation) {
+                $pdf->add_annotation($annotation->x,
+                                     $annotation->y,
+                                     $annotation->endx,
+                                     $annotation->endy,
+                                     $annotation->colour,
+                                     $annotation->type,
+                                     $annotation->path,
+                                     $stamptmpdir);
+            }
+        }
+
+        fulldelete($stamptmpdir);
+
+        $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber);
+        $filename = clean_param($filename, PARAM_FILE);
+
+        $generatedpdf = $tmpdir . '/' . $filename;
+        $pdf->save_pdf($generatedpdf);
+
+
+        $record = new \stdClass();
+
+        $record->contextid = $assignment->get_context()->id;
+        $record->component = 'assignfeedback_editpdf';
+        $record->filearea = self::FINAL_PDF_FILEAREA;
+        $record->itemid = $grade->id;
+        $record->filepath = '/';
+        $record->filename = $filename;
+
+
+        // Only keep one current version of the generated pdf.
+        $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
+
+        $file = $fs->create_file_from_pathname($record, $generatedpdf);
+
+        // Cleanup.
+        @unlink($generatedpdf);
+        @unlink($combined);
+        @rmdir($tmpdir);
+
+        return $file;
+    }
+
+    /**
+     * This function returns the generated pdf (if it exists).
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return stored_file
+     */
+    public static function get_feedback_document($assignment, $userid, $attemptnumber) {
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+
+        $contextid = $assignment->get_context()->id;
+        $component = 'assignfeedback_editpdf';
+        $filearea = self::FINAL_PDF_FILEAREA;
+        $itemid = $grade->id;
+        $filepath = '/';
+
+        $fs = \get_file_storage();
+        $files = $fs->get_area_files($contextid,
+                                     $component,
+                                     $filearea,
+                                     $itemid,
+                                     "itemid, filepath, filename",
+                                     false);
+        if ($files) {
+            return reset($files);
+        }
+        return false;
+    }
+
+    /**
+     * This function deletes the generated pdf for a student.
+     * @param int|\assign $assignment
+     * @param int $userid
+     * @param int $attemptnumber (-1 means latest attempt)
+     * @return bool
+     */
+    public static function delete_feedback_document($assignment, $userid, $attemptnumber) {
+
+        $assignment = self::get_assignment_from_param($assignment);
+
+        if (!$assignment->can_view_submission($userid)) {
+            \print_error('nopermission');
+        }
+        if (!$assignment->can_grade()) {
+            \print_error('nopermission');
+        }
+
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+
+        $contextid = $assignment->get_context()->id;
+        $component = 'assignfeedback_editpdf';
+        $filearea = self::FINAL_PDF_FILEAREA;
+        $itemid = $grade->id;
+
+        $fs = \get_file_storage();
+        return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
+    }
+
+}
diff --git a/mod/assign/feedback/editpdf/classes/page_editor.php b/mod/assign/feedback/editpdf/classes/page_editor.php
new file mode 100644 (file)
index 0000000..4b6d53a
--- /dev/null
@@ -0,0 +1,303 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the editor class for the assignfeedback_editpdf plugin
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf;
+
+/**
+ * This class performs crud operations on comments and annotations from a page of a response.
+ *
+ * No capability checks are done - they should be done by the calling class.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class page_editor {
+
+    /**
+     * Get all comments for a page.
+     * @param int $gradeid
+     * @param int $pageno
+     * @param bool $draft
+     * @return comment[]
+     */
+    public static function get_comments($gradeid, $pageno, $draft) {
+        global $DB;
+
+        $comments = array();
+        $params = array('gradeid'=>$gradeid, 'pageno'=>$pageno, 'draft'=>1);
+        if (!$draft) {
+            $params['draft'] = 0;
+        }
+        $records = $DB->get_records('assignfeedback_editpdf_cmnt', $params);
+        foreach ($records as $record) {
+            array_push($comments, new comment($record));
+        }
+
+        return $comments;
+    }
+
+    /**
+     * Set all comments for a page.
+     * @param int $gradeid
+     * @param int $pageno
+     * @param comment[] $comments
+     * @return int - the number of comments.
+     */
+    public static function set_comments($gradeid, $pageno, $comments) {
+        global $DB;
+
+        $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'pageno'=>$pageno, 'draft'=>1));
+
+        $added = 0;
+        foreach ($comments as $record) {
+            // Force these.
+            $comment = new comment($record);
+            if (trim($comment->rawtext) === '') {
+                continue;
+            }
+            $comment->gradeid = $gradeid;
+            $comment->pageno = $pageno;
+            $comment->draft = 1;
+            if (self::add_comment($comment)) {
+                $added++;
+            }
+        }
+
+        return $added;
+    }
+
+    /**
+     * Get a single comment by id.
+     * @param int $commentid
+     * @return comment or false
+     */
+    public static function get_comment($commentid) {
+        $record = $DB->get_record('assignfeedback_editpdf_cmnt', array('id'=>$commentid), '*', IGNORE_MISSING);
+        if ($record) {
+            return new comment($record);
+        }
+        return false;
+    }
+
+    /**
+     * Add a comment to a page.
+     * @param comment $comment
+     * @return bool
+     */
+    public static function add_comment(comment $comment) {
+        global $DB;
+        $comment->id = null;
+        return $DB->insert_record('assignfeedback_editpdf_cmnt', $comment);
+    }
+
+    /**
+     * Remove a comment from a page.
+     * @param int $commentid
+     * @return bool
+     */
+    public static function remove_comment($commentid) {
+        global $DB;
+        return $DB->delete_records('assignfeedback_editpdf_cmnt', array('id'=>$commentid));
+    }
+
+    /**
+     * Get all annotations for a page.
+     * @param int $gradeid
+     * @param int $pageno
+     * @param bool $draft
+     * @return annotation[]
+     */
+    public static function get_annotations($gradeid, $pageno, $draft) {
+        global $DB;
+
+        $params = array('gradeid'=>$gradeid, 'pageno'=>$pageno, 'draft'=>1);
+        if (!$draft) {
+            $params['draft'] = 0;
+        }
+        $annotations = array();
+        $records = $DB->get_records('assignfeedback_editpdf_annot', $params);
+        foreach ($records as $record) {
+            array_push($annotations, new annotation($record));
+        }
+
+        return $annotations;
+    }
+
+    /**
+     * Set all annotations for a page.
+     * @param int $gradeid
+     * @param int $pageno
+     * @param annotation[] $annotations
+     * @return int - the number of annotations.
+     */
+    public static function set_annotations($gradeid, $pageno, $annotations) {
+        global $DB;
+
+        $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'pageno'=>$pageno));
+        $added = 0;
+        foreach ($annotations as $record) {
+            // Force these.
+            $annotation = new annotation($record);
+            $annotation->gradeid = $gradeid;
+            $annotation->pageno = $pageno;
+            $annotation->draft = 1;
+            if (self::add_annotation($annotation)) {
+                $added++;
+            }
+        }
+
+        return $added;
+    }
+
+    /**
+     * Get a single annotation by id.
+     * @param int $annotationid
+     * @return annotation or false
+     */
+    public static function get_annotation($annotationid) {
+        global $DB;
+
+        $record = $DB->get_record('assignfeedback_editpdf_annot', array('id'=>$annotationid), '*', IGNORE_MISSING);
+        if ($record) {
+            return new annotation($record);
+        }
+        return false;
+    }
+
+    /**
+     * Unrelease drafts
+     * @param int $gradeid
+     * @return bool
+     */
+    public static function unrelease_drafts($gradeid) {
+        global $DB;
+
+        // Delete the non-draft annotations and comments.
+        $result = $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>0));
+        $result = $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>0)) && $result;
+        return $result;
+    }
+
+    /**
+     * Release the draft comments and annotations to students.
+     * @param int $gradeid
+     * @return bool
+     */
+    public static function release_drafts($gradeid) {
+        global $DB;
+
+        // Delete the previous non-draft annotations and comments.
+        $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>0));
+        $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>0));
+
+        // Copy all the draft annotations and comments to non-drafts.
+        $records = $DB->get_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>1));
+        foreach ($records as $record) {
+            unset($record->id);
+            $record->draft = 0;
+            $DB->insert_record('assignfeedback_editpdf_annot', $record);
+        }
+        $records = $DB->get_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>1));
+        foreach ($records as $record) {
+            unset($record->id);
+            $record->draft = 0;
+            $DB->insert_record('assignfeedback_editpdf_cmnt', $record);
+        }
+
+        return true;
+    }
+
+    /**
+     * Has annotations or comments.
+     * @param int $gradeid
+     * @return bool
+     */
+    public static function has_annotations_or_comments($gradeid, $includedraft) {
+        global $DB;
+        $params = array('gradeid'=>$gradeid);
+        if (!$includedraft) {
+            $params['draft'] = 0;
+        }
+        if ($DB->count_records('assignfeedback_editpdf_cmnt', $params)) {
+            return true;
+        }
+        if ($DB->count_records('assignfeedback_editpdf_annot', $params)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Aborts all draft annotations and reverts to the last version released to students.
+     * @param int $gradeid
+     * @return bool
+     */
+    public static function revert_drafts($gradeid) {
+        global $DB;
+
+        // Delete the previous non-draft annotations and comments.
+        $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>1));
+        $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>1));
+
+        // Copy all the draft annotations and comments to non-drafts.
+        $records = $DB->get_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>0));
+        foreach ($records as $record) {
+            unset($record->id);
+            $record->draft = 0;
+            $DB->insert_record('assignfeedback_editpdf_annot', $record);
+        }
+        $records = $DB->get_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>0));
+        foreach ($records as $record) {
+            unset($record->id);
+            $record->draft = 0;
+            $DB->insert_record('assignfeedback_editpdf_annot', $record);
+        }
+
+        return true;
+    }
+
+    /**
+     * Add a annotation to a page.
+     * @param annotation $annotation
+     * @return bool
+     */
+    public static function add_annotation(annotation $annotation) {
+        global $DB;
+
+        $annotation->id = null;
+        return $DB->insert_record('assignfeedback_editpdf_annot', $annotation);
+    }
+
+    /**
+     * Remove a annotation from a page.
+     * @param int $annotationid
+     * @return bool
+     */
+    public static function remove_annotation($annotationid) {
+        global $DB;
+
+        return $DB->delete_records('assignfeedback_editpdf_annot', array('id'=>$annotationid));
+    }
+}
diff --git a/mod/assign/feedback/editpdf/classes/pdf.php b/mod/assign/feedback/editpdf/classes/pdf.php
new file mode 100644 (file)
index 0000000..f83a5e1
--- /dev/null
@@ -0,0 +1,533 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Library code for manipulating PDFs
+ *
+ * @package assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir.'/pdflib.php');
+require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/fpdi.php');
+
+/**
+ * Library code for manipulating PDFs
+ *
+ * @package assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class pdf extends \FPDI {
+
+    /** @var int the number of the current page in the PDF being processed */
+    protected $currentpage = 0;
+    /** @var int the total number of pages in the PDF being processed */
+    protected $pagecount = 0;
+    /** @var float used to scale the pixel position of annotations (in the database) to the position in the final PDF */
+    protected $scale = 0.0;
+    /** @var string the path in which to store generated page images */
+    protected $imagefolder = null;
+    /** @var string the path to the PDF currently being processed */
+    protected $filename = null;
+
+    /** No errors */
+    const GSPATH_OK = 'ok';
+    /** Not set */
+    const GSPATH_EMPTY = 'empty';
+    /** Does not exist */
+    const GSPATH_DOESNOTEXIST = 'doesnotexist';
+    /** Is a dir */
+    const GSPATH_ISDIR = 'isdir';
+    /** Not executable */
+    const GSPATH_NOTEXECUTABLE = 'notexecutable';
+    /** Test file missing */
+    const GSPATH_NOTESTFILE = 'notestfile';
+    /** Any other error */
+    const GSPATH_ERROR = 'error';
+
+    /**
+     * Combine the given PDF files into a single PDF. Optionally add a coversheet and coversheet fields.
+     * @param string[] $pdflist  the filenames of the files to combine
+     * @param string $outfilename the filename to write to
+     * @return int the number of pages in the combined PDF
+     */
+    public function combine_pdfs($pdflist, $outfilename) {
+
+        $this->setPageUnit('pt');
+        $this->setPrintHeader(false);
+        $this->setPrintFooter(false);
+        $this->scale = 72.0 / 100.0;
+        $this->SetFont('helvetica', '', 16.0 * $this->scale);
+        $this->SetTextColor(0, 0, 0);
+
+        $totalpagecount = 0;
+
+        foreach ($pdflist as $file) {
+            $pagecount = $this->setSourceFile($file);
+            $totalpagecount += $pagecount;
+            for ($i = 1; $i<=$pagecount; $i++) {
+                $this->create_page_from_source($i);
+            }
+        }
+
+        $this->save_pdf($outfilename);
+
+        return $totalpagecount;
+    }
+
+    /**
+     * The number of the current page in the PDF being processed
+     * @return int
+     */
+    public function current_page() {
+        return $this->currentpage;
+    }
+
+    /**
+     * The total number of pages in the PDF being processed
+     * @return int
+     */
+    public function page_count() {
+        return $this->pagecount;
+    }
+
+    /**
+     * Load the specified PDF and set the initial output configuration
+     * Used when processing comments and outputting a new PDF
+     * @param string $filename the path to the PDF to load
+     * @return int the number of pages in the PDF
+     */
+    public function load_pdf($filename) {
+        $this->setPageUnit('pt');
+        $this->scale = 72.0 / 100.0;
+        $this->SetFont('helvetica', '', 16.0 * $this->scale);
+        $this->SetFillColor(255, 255, 176);
+        $this->SetDrawColor(0, 0, 0);
+        $this->SetLineWidth(1.0 * $this->scale);
+        $this->SetTextColor(0, 0, 0);
+        $this->setPrintHeader(false);
+        $this->setPrintFooter(false);
+        $this->pagecount = $this->setSourceFile($filename);
+        $this->filename = $filename;
+        return $this->pagecount;
+    }
+
+    /**
+     * Sets the name of the PDF to process, but only loads the file if the
+     * pagecount is zero (in order to count the number of pages)
+     * Used when generating page images (but not a new PDF)
+     * @param string $filename the path to the PDF to process
+     * @param int $pagecount optional the number of pages in the PDF, if known
+     * @return int the number of pages in the PDF
+     */
+    public function set_pdf($filename, $pagecount = 0) {
+        if ($pagecount == 0) {
+            return $this->load_pdf($filename);
+        } else {
+            $this->filename = $filename;
+            $this->pagecount = $pagecount;
+            return $pagecount;
+        }
+    }
+
+    /**
+     * Copy the next page from the source file and set it as the current page
+     * @return bool true if successful
+     */
+    public function copy_page() {
+        if (!$this->filename) {
+            return false;
+        }
+        if ($this->currentpage>=$this->pagecount) {
+            return false;
+        }
+        $this->currentpage++;
+        $this->create_page_from_source($this->currentpage);
+        return true;
+    }
+
+    /**
+     * Create a page from a source PDF.
+     *
+     * @param int $pageno
+     */
+    protected function create_page_from_source($pageno) {
+        // Get the size (and deduce the orientation) of the next page.
+        $template = $this->importPage($pageno);
+        $size = $this->getTemplateSize($template);
+        $orientation = 'P';
+        if ($size['w'] > $size['h']) {
+            $orientation = 'L';
+        }
+        // Create a page of the required size / orientation.
+        $this->AddPage($orientation, array($size['w'], $size['h']));
+        // Prevent new page creation when comments are at the bottom of a page.
+        $this->setPageOrientation($orientation, false, 0);
+        // Fill in the page with the original contents from the student.
+        $this->useTemplate($template);
+    }
+
+    /**
+     * Copy all the remaining pages in the file
+     */
+    public function copy_remaining_pages() {
+        $morepages = true;
+        while ($morepages) {
+            $morepages = $this->copy_page();
+        }
+    }
+
+    /**
+     * Add a comment to the current page
+     * @param string $text the text of the comment
+     * @param int $x the x-coordinate of the comment (in pixels)
+     * @param int $y the y-coordinate of the comment (in pixels)
+     * @param int $width the width of the comment (in pixels)
+     * @param string $colour optional the background colour of the comment (red, yellow, green, blue, white, clear)
+     * @return bool true if successful (always)
+     */
+    public function add_comment($text, $x, $y, $width, $colour = 'yellow') {
+        if (!$this->filename) {
+            return false;
+        }
+        switch ($colour) {
+            case 'red':
+                $this->SetFillColor(255, 176, 176);
+                break;
+            case 'green':
+                $this->SetFillColor(176, 255, 176);
+                break;
+            case 'blue':
+                $this->SetFillColor(208, 208, 255);
+                break;
+            case 'white':
+                $this->SetFillColor(255, 255, 255);
+                break;
+            default: /* Yellow */
+                $this->SetFillColor(255, 255, 176);
+                break;
+        }
+
+        $x *= $this->scale;
+        $y *= $this->scale;
+        $width *= $this->scale;
+        $text = str_replace('&lt;', '<', $text);
+        $text = str_replace('&gt;', '>', $text);
+        // Draw the text with a border, but no background colour (using a background colour would cause the fill to
+        // appear behind any existing content on the page, hence the extra filled rectangle drawn below).
+        $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */
+        if ($colour != 'clear') {
+            $newy = $this->GetY();
+            // Now we know the final size of the comment, draw a rectangle with the background colour.
+            $this->Rect($x, $y, $width, $newy - $y, 'DF');
+            // Re-draw the text over the top of the background rectangle.
+            $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */
+        }
+        return true;
+    }
+
+    /**
+     * Add an annotation to the current page
+     * @param int $sx starting x-coordinate (in pixels)
+     * @param int $sy starting y-coordinate (in pixels)
+     * @param int $ex ending x-coordinate (in pixels)
+     * @param int $ey ending y-coordinate (in pixels)
+     * @param string $colour optional the colour of the annotation (red, yellow, green, blue, white, black)
+     * @param string $type optional the type of annotation (line, oval, rectangle, highlight, pen, stamp)
+     * @param int[]|string $path optional for 'pen' annotations this is an array of x and y coordinates for
+     *              the line, for 'stamp' annotations it is the name of the stamp file (without the path)
+     * @param string $imagefolder - Folder containing stamp images.
+     * @return bool true if successful (always)
+     */
+    public function add_annotation($sx, $sy, $ex, $ey, $colour = 'yellow', $type = 'line', $path, $imagefolder) {
+        global $CFG;
+        if (!$this->filename) {
+            return false;
+        }
+        switch ($colour) {
+            case 'yellow':
+                $colourarray = array(255, 255, 0);
+                break;
+            case 'green':
+                $colourarray = array(0, 255, 0);
+                break;
+            case 'blue':
+                $colourarray = array(0, 0, 255);
+                break;
+            case 'white':
+                $colourarray = array(255, 255, 255);
+                break;
+            case 'black':
+                $colourarray = array(0, 0, 0);
+                break;
+            default: /* Red */
+                $colour = 'red';
+                $colourarray = array(255, 0, 0);
+                break;
+        }
+        $this->SetDrawColorArray($colourarray);
+
+        $sx *= $this->scale;
+        $sy *= $this->scale;
+        $ex *= $this->scale;
+        $ey *= $this->scale;
+
+        $this->SetLineWidth(3.0 * $this->scale);
+        switch ($type) {
+            case 'oval':
+                $rx = abs($sx - $ex) / 2;
+                $ry = abs($sy - $ey) / 2;
+                $sx = min($sx, $ex) + $rx;
+                $sy = min($sy, $ey) + $ry;
+                $this->Ellipse($sx, $sy, $rx, $ry);
+                break;
+            case 'rectangle':
+                $w = abs($sx - $ex);
+                $h = abs($sy - $ey);
+                $sx = min($sx, $ex);
+                $sy = min($sy, $ey);
+                $this->Rect($sx, $sy, $w, $h);
+                break;
+            case 'highlight':
+                $w = abs($sx - $ex);
+                $h = 8.0 * $this->scale;
+                $sx = min($sx, $ex);
+                $sy = min($sy, $ey) + ($h * 0.5);
+                $this->SetAlpha(0.5, 'Normal', 0.5, 'Normal');
+                $this->SetLineWidth(8.0 * $this->scale);
+                $this->Rect($sx, $sy, $w, $h);
+                $this->SetAlpha(1.0, 'Normal', 1.0, 'Normal');
+                break;
+            case 'pen':
+                if ($path) {
+                    $scalepath = array();
+                    $points = preg_split('/[,:]/', $path);
+                    foreach ($points as $point) {
+                        $scalepath[] = intval($point) * $this->scale;
+                    }
+                    $this->PolyLine($scalepath, 'S');
+                }
+                break;
+            case 'stamp':
+                $imgfile = $imagefolder . '/' . clean_filename($path);
+                $w = abs($sx - $ex);
+                $h = abs($sy - $ey);
+                $sx = min($sx, $ex);
+                $sy = min($sy, $ey);
+                $this->Image($imgfile, $sx, $sy, $w, $h);
+                break;
+            default: // Line.
+                $this->Line($sx, $sy, $ex, $ey);
+                break;
+        }
+        $this->SetDrawColor(0, 0, 0);
+        $this->SetLineWidth(1.0 * $this->scale);
+
+        return true;
+    }
+
+    /**
+     * Save the completed PDF to the given file
+     * @param string $filename the filename for the PDF (including the full path)
+     */
+    public function save_pdf($filename) {
+        $this->Output($filename, 'F');
+    }
+
+    /**
+     * Set the path to the folder in which to generate page image files
+     * @param string $folder
+     */
+    public function set_image_folder($folder) {
+        $this->imagefolder = $folder;
+    }
+
+    /**
+     * Generate an image of the specified page in the PDF
+     * @param int $pageno the page to generate the image of
+     * @throws moodle_exception
+     * @throws coding_exception
+     * @return string the filename of the generated image
+     */
+    public function get_image($pageno) {
+        if (!$this->filename) {
+            throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename');
+        }
+
+        if (!$this->imagefolder) {
+            throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder');
+        }
+
+        if (!is_dir($this->imagefolder)) {
+            throw new \coding_exception('The specified image output folder is not a valid folder');
+        }
+
+        $imagefile = $this->imagefolder.'/image_page' . $pageno . '.png';
+        $generate = true;
+        if (file_exists($imagefile)) {
+            if (filemtime($imagefile)>filemtime($this->filename)) {
+                // Make sure the image is newer than the PDF file.
+                $generate = false;
+            }
+        }
+
+        if ($generate) {
+            // Use ghostscript to generate an image of the specified page.
+            $gsexec = \get_config('assignfeedback_editpdf', 'gspath');
+            $imageres = 100;
+            $filename = $this->filename;
+            $pagenoinc = $pageno + 1;
+            $command = "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$pagenoinc -dLastPage=$pagenoinc ".
+                "-dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=\"$imagefile\" \"$filename\"";
+
+            //$command = escapeshellcmd($command);
+            $result = exec($command);
+            if (!file_exists($imagefile)) {
+                $fullerror = 'Command:' . ($command) . '<br/>';
+                $fullerror .= 'Result:' . htmlspecialchars($result) . '<br/>';
+                throw new \moodle_exception('errorgenerateimage', 'assignfeedback_pdf', '', $fullerror);
+            }
+        }
+
+        return 'image_page'.$pageno.'.png';
+    }
+
+    /**
+     * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it
+     * @param stored_file $file
+     * @return string path to copy or converted pdf (false == fail)
+     */
+    public static function ensure_pdf_compatible(\stored_file $file) {
+        global $CFG;
+
+        $fp = $file->get_content_file_handle();
+        $ident = fread($fp, 10);
+        if (substr_compare('%PDF-', $ident, 0, 5) !== 0) {
+            return false; // This is not a PDF file at all.
+        }
+        $ident = substr($ident, 5); // Remove the '%PDF-' part.
+        $ident = explode('\x0A', $ident); // Truncate to first '0a' character.
+        list($major, $minor) = explode('.', $ident[0]); // Split the major / minor version.
+        $major = intval($major);
+        $minor = intval($minor);
+        if ($major == 0 || $minor == 0) {
+            return false; // Not a valid PDF version number.
+        }
+        $temparea = \make_temp_directory('assignfeedback_editpdf');
+        $hash = $file->get_contenthash(); // Use the contenthash to make sure the temp files have unique names.
+        $tempsrc = $temparea . "/src-$hash.pdf";
+        $tempdst = $temparea . "/dst-$hash.pdf";
+
+        if ($major = 1 && $minor<=4) {
+            // PDF is valid version - just create a copy we can use.
+            $file->copy_content_to($tempdst); // Copy the file.
+            return $tempdst;
+        }
+
+        $file->copy_content_to($tempsrc); // Copy the file.
+
+        $gsexec = \get_config('assignfeedback_editpdf', 'gspath');
+        $command = "$gsexec -q -sDEVICE=pdfwrite -dBATCH -dNOPAUSE -sOutputFile=\"$tempdst\" \"$tempsrc\"";
+        //$command = escapeshellcmd($command);
+        exec($command);
+        @unlink($tempsrc);
+        if (!file_exists($tempdst)) {
+            // Something has gone wrong in the conversion.
+            return false;
+        }
+
+        return $tempdst;
+    }
+
+    /**
+     * Test that the configured path to ghostscript is correct and working.
+     * @param bool $generateimage - If true - a test image will be generated to verify the install.
+     * @return bool
+     */
+    public static function test_gs_path($generateimage = true) {
+        global $CFG;
+
+        $ret = (object)array(
+            'status' => self::GSPATH_OK,
+            'message' => null,
+        );
+        $gspath = \get_config('assignfeedback_editpdf', 'gspath');
+        if (empty($gspath)) {
+            $ret->status = self::GSPATH_EMPTY;
+            return $ret;
+        }
+        if (!file_exists($gspath)) {
+            $ret->status = self::GSPATH_DOESNOTEXIST;
+            return $ret;
+        }
+        if (is_dir($gspath)) {
+            $ret->status = self::GSPATH_ISDIR;
+            return $ret;
+        }
+        if (!is_executable($gspath)) {
+            $ret->status = self::GSPATH_NOTEXECUTABLE;
+            return $ret;
+        }
+
+        $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf';
+        if (!file_exists($testfile)) {
+            $ret->status = self::GSPATH_NOTESTFILE;
+            return $ret;
+        }
+
+        if (!$generateimage) {
+            return $ret;
+        }
+
+        $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
+        @unlink($testimagefolder.'/image_page0.png'); // Delete any previous test images.
+
+        $pdf = new pdf();
+        $pdf->set_pdf($testfile);
+        $pdf->set_image_folder($testimagefolder);
+        try {
+            $pdf->get_image(0);
+        } catch (\moodle_exception $e) {
+            $ret->status = self::GSPATH_ERROR;
+            $ret->message = $e->getMessage();
+        }
+
+        return $ret;
+    }
+
+    /**
+     * If the test image has been generated correctly - send it direct to the browser.
+     */
+    public static function send_test_image() {
+        global $CFG;
+        header('Content-type: image/png');
+        require_once($CFG->libdir.'/filelib.php');
+
+        $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
+        $testimage = $testimagefolder.'/image_page0.png';
+        send_file($testimage, basename($testimage));
+        die();
+    }
+
+}
+
diff --git a/mod/assign/feedback/editpdf/classes/renderer.php b/mod/assign/feedback/editpdf/classes/renderer.php
new file mode 100644 (file)
index 0000000..b28f7fc
--- /dev/null
@@ -0,0 +1,247 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the definition for the library class for edit PDF renderer.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A custom renderer class that extends the plugin_renderer_base and is used by the editpdf feedback plugin.
+ *
+ * @package assignfeedback_editpdf
+ * @copyright 2013 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_editpdf_renderer extends plugin_renderer_base {
+
+    /**
+     * Return the PDF button shortcut.
+     *
+     * @param string $name the name of a specific button.
+     * @return string the specific shortcut.
+     */
+    private function get_shortcut($name) {
+
+        $shortcuts = array('navigate-previous-button' => 'j',
+            'navigate-page-select' => 'k',
+            'navigate-next-button' => 'l',
+            'searchcomments' => 'h',
+            'comment' => 'z',
+            'commentcolour' => 'x',
+            'select' => 'c',
+            'pen' => 'y',
+            'line' => 'u',
+            'rectangle' => 'i',
+            'oval' => 'o',
+            'highlight' => 'p',
+            'annotationcolour' => 'r',
+            'stamp' => 'n',
+            'currentstamp' => 'm');
+
+
+        // Return the shortcut.
+        return $shortcuts[$name];
+    }
+
+    /**
+     * Render a single colour button.
+     *
+     * @param string $icon - The key for the icon
+     * @param string $tool - The key for the lang string.
+     * @param string $accesskey Optional - The access key for the button.
+     * @param bool $disabled Optional - Is this button disabled.
+     * @return string
+     */
+    private function render_toolbar_button($icon, $tool, $accesskey = null, $disabled=false) {
+
+        // Build button alt text.
+        $alttext = new stdClass();
+        $alttext->tool = $tool;
+        if (!empty($accesskey)) {
+            $alttext->shortcut = '(Alt/Shift-Alt/Ctrl-Option + ' . $accesskey . ')';
+        } else {
+            $alttext->shortcut = '';
+        }
+        $iconalt = get_string('toolbarbutton', 'assignfeedback_editpdf', $alttext);
+
+        $iconhtml = $this->pix_icon($icon, $iconalt, 'assignfeedback_editpdf');
+        $iconparams = array('data-tool'=>$tool, 'class'=>$tool . 'button');
+        if ($disabled) {
+            $iconparams['disabled'] = 'true';
+        }
+        if (!empty($accesskey)) {
+            $iconparams['accesskey'] = $accesskey;
+        }
+
+        return html_writer::tag('button', $iconhtml, $iconparams);
+    }
+
+    /**
+     * Render the editpdf widget in the grading form.
+     *
+     * @param assignfeedback_editpdf_widget $widget - Renderable widget containing assignment, user and attempt number.
+     * @return string
+     */
+    public function render_assignfeedback_editpdf_widget(assignfeedback_editpdf_widget $widget) {
+        global $CFG;
+
+        $html = '';
+
+        $html .= html_writer::div(get_string('jsrequired', 'assignfeedback_editpdf'), 'hiddenifjs');
+        $linkid = html_writer::random_id();
+        if ($widget->readonly) {
+            $launcheditorlink = html_writer::tag('a',
+                                              get_string('viewfeedbackonline', 'assignfeedback_editpdf'),
+                                              array('id'=>$linkid, 'class'=>'btn', 'href'=>'#'));
+        } else {
+            $launcheditorlink = html_writer::tag('a',
+                                              get_string('launcheditor', 'assignfeedback_editpdf'),
+                                              array('id'=>$linkid, 'class'=>'btn', 'href'=>'#'));
+        }
+        $links = $launcheditorlink;
+
+        $links .= html_writer::tag('div',
+                                   get_string('unsavedchanges', 'assignfeedback_editpdf'),
+                                   array('class'=>'assignfeedback_editpdf_unsavedchanges warning'));
+
+        $html .= html_writer::div($links, 'visibleifjs');
+        $header = get_string('pluginname', 'assignfeedback_editpdf');
+        $body = '';
+        // Create the page navigation.
+        $navigation1 = '';
+        $navigation2 = '';
+
+        // Pick the correct arrow icons for right to left mode.
+        if (right_to_left()) {
+            $nav_prev = 'nav_next';
+            $nav_next = 'nav_prev';
+        } else {
+            $nav_prev = 'nav_prev';
+            $nav_next = 'nav_next';
+        }
+
+        $iconalt = get_string('navigateprevious', 'assignfeedback_editpdf');
+        $iconhtml = $this->pix_icon($nav_prev, $iconalt, 'assignfeedback_editpdf');
+        $navigation1 .= html_writer::tag('button', $iconhtml, array('disabled'=>'true',
+            'class'=>'navigate-previous-button', 'accesskey' => $this->get_shortcut('navigate-previous-button')));
+        $pageoptions = html_writer::tag('option', get_string('gotopage', 'assignfeedback_editpdf'), array('value'=>''));
+        $navigation1 .= html_writer::tag('select', $pageoptions, array('disabled'=>'true',
+            'class'=>'navigate-page-select', 'accesskey' => $this->get_shortcut('navigate-page-select')));
+        $iconalt = get_string('navigatenext', 'assignfeedback_editpdf');
+        $iconhtml = $this->pix_icon($nav_next, $iconalt, 'assignfeedback_editpdf');
+        $navigation1 .= html_writer::tag('button', $iconhtml, array('disabled'=>'true',
+            'class'=>'navigate-next-button', 'accesskey' => $this->get_shortcut('navigate-next-button')));
+
+        $navigation1 = html_writer::div($navigation1, 'navigation', array('role'=>'navigation'));
+
+        $navigation2 .= $this->render_toolbar_button('comment_search', 'searchcomments', $this->get_shortcut('searchcomments'));
+        $navigation2 = html_writer::div($navigation2, 'navigation-search', array('role'=>'navigation'));
+
+        $toolbar1 = '';
+        $toolbar2 = '';
+        $toolbar3 = '';
+        $toolbar4 = '';
+        $clearfix = html_writer::div('', 'clearfix');
+        if (!$widget->readonly) {
+
+            // Comments.
+            $toolbar1 .= $this->render_toolbar_button('comment', 'comment', $this->get_shortcut('comment'));
+            $toolbar1 .= $this->render_toolbar_button('background_colour_clear', 'commentcolour', $this->get_shortcut('commentcolour'));
+            $toolbar1 = html_writer::div($toolbar1, 'toolbar', array('role'=>'toolbar'));
+
+            // Select Tool.
+            $toolbar2 .= $this->render_toolbar_button('select', 'select', $this->get_shortcut('select'));
+            $toolbar2 = html_writer::div($toolbar2, 'toolbar', array('role'=>'toolbar'));
+
+            // Other Tools.
+            $toolbar3 = $this->render_toolbar_button('pen', 'pen', $this->get_shortcut('pen'));
+            $toolbar3 .= $this->render_toolbar_button('line', 'line', $this->get_shortcut('line'));
+            $toolbar3 .= $this->render_toolbar_button('rectangle', 'rectangle', $this->get_shortcut('rectangle'));
+            $toolbar3 .= $this->render_toolbar_button('oval', 'oval', $this->get_shortcut('oval'));
+            $toolbar3 .= $this->render_toolbar_button('highlight', 'highlight', $this->get_shortcut('highlight'));
+            $toolbar3 .= $this->render_toolbar_button('background_colour_clear', 'annotationcolour', $this->get_shortcut('annotationcolour'));
+            $toolbar3 = html_writer::div($toolbar3, 'toolbar', array('role'=>'toolbar'));
+
+            // Stamps.
+            $toolbar4 .= $this->render_toolbar_button('stamp', 'stamp', 'n');
+            $toolbar4 .= $this->render_toolbar_button('background_colour_clear', 'currentstamp', $this->get_shortcut('currentstamp'));
+            $toolbar4 = html_writer::div($toolbar4, 'toolbar', array('role'=>'toolbar'));
+        }
+
+        // Toobars written in reverse order because they are floated right.
+        $pageheader = html_writer::div($navigation1 .
+                                       $navigation2 .
+                                       $toolbar4 .
+                                       $toolbar3 .
+                                       $toolbar2 .
+                                       $toolbar1 .
+                                       $clearfix,
+                                       'pageheader');
+        $body = $pageheader;
+
+        $loading = $this->pix_icon('i/loading', get_string('loadingeditor', 'assignfeedback_editpdf'), 'moodle', array('class'=>'loading'));
+        $canvas = html_writer::div($loading, 'drawingcanvas');
+        $body .= html_writer::div($canvas, 'drawingregion');
+
+        $body .= '<hr/>';
+
+        $footer = '';
+
+        $editorparams = array(array('header'=>$header,
+                                    'body'=>$body,
+                                    'footer'=>$footer,
+                                    'linkid'=>$linkid,
+                                    'assignmentid'=>$widget->assignment,
+                                    'userid'=>$widget->userid,
+                                    'attemptnumber'=>$widget->attemptnumber,
+                                    'stampfiles'=>$widget->stampfiles,
+                                    'readonly'=>$widget->readonly));
+
+        $this->page->requires->yui_module('moodle-assignfeedback_editpdf-editor',
+                                          'M.assignfeedback_editpdf.editor.init',
+                                          $editorparams);
+
+        $this->page->requires->strings_for_js(array(
+            'yellow',
+            'white',
+            'red',
+            'blue',
+            'green',
+            'black',
+            'clear',
+            'colourpicker',
+            'loadingeditor',
+            'pagexofy',
+            'deletecomment',
+            'addtoquicklist',
+            'filter',
+            'searchcomments',
+            'commentcontextmenu',
+            'deleteannotation',
+            'stamp',
+            'stamppicker',
+            'cannotopenpdf'
+        ), 'assignfeedback_editpdf');
+
+        return $html;
+    }
+}
diff --git a/mod/assign/feedback/editpdf/classes/widget.php b/mod/assign/feedback/editpdf/classes/widget.php
new file mode 100644 (file)
index 0000000..5fba725
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the definition for the library class for edit PDF renderer.
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A custom renderer class that extends the plugin_renderer_base and is used by the editpdf feedback plugin.
+ *
+ * @package assignfeedback_editpdf
+ * @copyright 2013 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_editpdf_widget implements renderable {
+
+    /** @var int $assignment - Assignment instance id */
+    public $assignment = 0;
+    /** @var int $userid - The user id we are grading */
+    public $userid = 0;
+    /** @var mixed $attemptnumber - The attempt number we are grading */
+    public $attemptnumber = 0;
+    /** @var moodle_url $downloadurl */
+    public $downloadurl = null;
+    /** @var string $downloadfilename */
+    public $downloadfilename = null;
+    /** @var string[] $stampfiles */
+    public $stampfiles = array();
+    /** @var bool $readonly */
+    public $readonly = true;
+
+    /**
+     * Constructor
+     * @param int $assignment - Assignment instance id
+     * @param int $userid - The user id we are grading
+     * @param int $attemptnumber - The attempt number we are grading
+     * @param moodle_url $downloadurl - A url to download the current generated pdf.
+     * @param string $downloadfilename - Name of the generated pdf.
+     * @param string[] $stampfiles - The file names of the stamps.
+     * @param bool $readonly - Show the readonly interface (no tools).
+     */
+    public function __construct($assignment, $userid, $attemptnumber, $downloadurl, $downloadfilename, $stampfiles, $readonly) {
+        $this->assignment = $assignment;
+        $this->userid = $userid;
+        $this->attemptnumber = $attemptnumber;
+        $this->downloadurl = $downloadurl;
+        $this->downloadfilename = $downloadfilename;
+        $this->stampfiles = $stampfiles;
+        $this->readonly = $readonly;
+    }
+}
diff --git a/mod/assign/feedback/editpdf/db/install.xml b/mod/assign/feedback/editpdf/db/install.xml
new file mode 100755 (executable)
index 0000000..79054b2
--- /dev/null
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="mod/assign/feedback/editpdf/db" VERSION="20130926" COMMENT="XMLDB file for Moodle mod/assign/feedback/editpdf"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
+>
+  <TABLES>
+    <TABLE NAME="assignfeedback_editpdf_cmnt" COMMENT="Stores comments added to pdfs">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="gradeid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="x" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="x-position of the top-left corner of the comment (in pixels - image resolution is set to 100 pixels per inch)"/>
+        <FIELD NAME="y" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="y-position of the top-left corner of the comment (in pixels - image resolution is set to 100 pixels per inch)"/>
+        <FIELD NAME="width" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="120" SEQUENCE="false" COMMENT="width, in pixels, of the comment box"/>
+        <FIELD NAME="rawtext" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Raw text of the comment"/>
+        <FIELD NAME="pageno" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The page in the PDF that this comment appears on"/>
+        <FIELD NAME="colour" TYPE="char" LENGTH="10" NOTNULL="false" DEFAULT="black" SEQUENCE="false" COMMENT="Can be red, yellow, green, blue, white, black"/>
+        <FIELD NAME="draft" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Is this a draft comment?"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="gradeid" TYPE="foreign" FIELDS="gradeid" REFTABLE="assign_grade" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="gradeid_pageno" UNIQUE="false" FIELDS="gradeid, pageno"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="assignfeedback_editpdf_annot" COMMENT="stores annotations added to pdfs submitted by students">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="gradeid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="pageno" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The page in the PDF that this annotation appears on"/>
+        <FIELD NAME="x" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="x-position of the start of the annotation (in pixels - image resolution is set to 100 pixels per inch)"/>
+        <FIELD NAME="y" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="y-position of the start of the annotation (in pixels - image resolution is set to 100 pixels per inch)"/>
+        <FIELD NAME="endx" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="x-position of the end of the annotation"/>
+        <FIELD NAME="endy" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="y-position of the end of the annotation"/>
+        <FIELD NAME="path" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="SVG path describing the freehand line"/>
+        <FIELD NAME="type" TYPE="char" LENGTH="10" NOTNULL="false" DEFAULT="line" SEQUENCE="false" COMMENT="line, oval, rect, etc."/>
+        <FIELD NAME="colour" TYPE="char" LENGTH="10" NOTNULL="false" DEFAULT="black" SEQUENCE="false" COMMENT="Can be red, yellow, green, blue, white, black"/>
+        <FIELD NAME="draft" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Is this a draft annotation?"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="gradeid" TYPE="foreign" FIELDS="gradeid" REFTABLE="assign_grade" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="gradeid_pageno" UNIQUE="false" FIELDS="gradeid, pageno"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="assignfeedback_editpdf_quick" COMMENT="Stores teacher specified quicklist comments">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="rawtext" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="width" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="120" SEQUENCE="false"/>
+        <FIELD NAME="colour" TYPE="char" LENGTH="10" NOTNULL="false" DEFAULT="yellow" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+  </TABLES>
+</XMLDB>
\ No newline at end of file
diff --git a/mod/assign/feedback/editpdf/fpdi/ASL20.txt b/mod/assign/feedback/editpdf/fpdi/ASL20.txt
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85.php b/mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85.php
new file mode 100644 (file)
index 0000000..a86c91f
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+//
+//  FPDI - Version 1.4.4
+//
+//    Copyright 2004-2013 Setasign - Jan Slabon
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+
+if (!defined('ORD_z'))
+       define('ORD_z',ord('z'));
+if (!defined('ORD_exclmark'))
+       define('ORD_exclmark', ord('!'));
+if (!defined('ORD_u')) 
+       define('ORD_u', ord('u'));
+if (!defined('ORD_tilde'))
+       define('ORD_tilde', ord('~'));
+
+if (!class_exists('FilterASCII85', false)) {
+
+    class FilterASCII85 {
+        
+        function error($msg) {
+            die($msg);
+        }
+        
+        function decode($in) {
+            $out = '';
+            $state = 0;
+            $chn = null;
+            
+            $l = strlen($in);
+            
+            for ($k = 0; $k < $l; ++$k) {
+                $ch = ord($in[$k]) & 0xff;
+                
+                if ($ch == ORD_tilde) {
+                    break;
+                }
+                if (preg_match('/^\s$/',chr($ch))) {
+                    continue;
+                }
+                if ($ch == ORD_z && $state == 0) {
+                    $out .= chr(0) . chr(0) . chr(0) . chr(0);
+                    continue;
+                }
+                if ($ch < ORD_exclmark || $ch > ORD_u) {
+                    return $this->error('Illegal character in ASCII85Decode.');
+                }
+                
+                $chn[$state++] = $ch - ORD_exclmark;
+                
+                if ($state == 5) {
+                    $state = 0;
+                    $r = 0;
+                    for ($j = 0; $j < 5; ++$j)
+                        $r = $r * 85 + $chn[$j];
+                    $out .= chr($r >> 24);
+                    $out .= chr($r >> 16);
+                    $out .= chr($r >> 8);
+                    $out .= chr($r);
+                }
+            }
+            $r = 0;
+            
+            if ($state == 1)
+                return $this->error('Illegal length in ASCII85Decode.');
+            if ($state == 2) {
+                $r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1]+1) * 85 * 85 * 85;
+                $out .= chr($r >> 24);
+            }
+            else if ($state == 3) {
+                $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85  + ($chn[2]+1) * 85 * 85;
+                $out .= chr($r >> 24);
+                $out .= chr($r >> 16);
+            }
+            else if ($state == 4) {
+                $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85  + $chn[2] * 85 * 85  + ($chn[3]+1) * 85 ;
+                $out .= chr($r >> 24);
+                $out .= chr($r >> 16);
+                $out .= chr($r >> 8);
+            }
+    
+            return $out;
+        }
+        
+        function encode($in) {
+            return $this->error("ASCII85 encoding not implemented.");
+        }
+    }
+}
diff --git a/mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85_FPDI.php b/mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85_FPDI.php
new file mode 100644 (file)
index 0000000..03413eb
--- /dev/null
@@ -0,0 +1,33 @@
+<?php\r
+//\r
+//  FPDI - Version 1.4.4\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+require_once('FilterASCII85.php');\r
+\r
+class FilterASCII85_FPDI extends FilterASCII85 {\r
+\r
+    var $fpdi;\r
+    \r
+    function FilterASCII85_FPDI(&$fpdi) {\r
+        $this->fpdi =& $fpdi;\r
+    }\r
+\r
+    function error($msg) {\r
+        $this->fpdi->error($msg);\r
+    }\r
+}
\ No newline at end of file
diff --git a/mod/assign/feedback/editpdf/fpdi/filters/FilterLZW.php b/mod/assign/feedback/editpdf/fpdi/filters/FilterLZW.php
new file mode 100644 (file)
index 0000000..d3d3333
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+//
+//  FPDI - Version 1.4.4
+//
+//    Copyright 2004-2013 Setasign - Jan Slabon
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+
+if (!class_exists('FilterLZW', false)) {
+
+    class FilterLZW {
+        
+        var $sTable = array();
+        var $data = null;
+        var $dataLength = 0;
+        var $tIdx;
+        var $bitsToGet = 9;
+        var $bytePointer;
+        var $bitPointer;
+        var $nextData = 0;
+        var $nextBits = 0;
+        var $andTable = array(511, 1023, 2047, 4095);
+    
+        function error($msg) {
+            die($msg);
+        }
+        
+        /**
+         * Method to decode LZW compressed data.
+         *
+         * @param string data    The compressed data.
+         */
+        function decode($data) {
+    
+            if($data[0] == 0x00 && $data[1] == 0x01) {
+                $this->error('LZW flavour not supported.');
+            }
+    
+            $this->initsTable();
+    
+            $this->data = $data;
+            $this->dataLength = strlen($data);
+    
+            // Initialize pointers
+            $this->bytePointer = 0;
+            $this->bitPointer = 0;
+    
+            $this->nextData = 0;
+            $this->nextBits = 0;
+    
+            $oldCode = 0;
+    
+            $string = '';
+            $uncompData = '';
+    
+            while (($code = $this->getNextCode()) != 257) {
+                if ($code == 256) {
+                    $this->initsTable();
+                    $code = $this->getNextCode();
+    
+                    if ($code == 257) {
+                        break;
+                    }
+    
+                    $uncompData .= $this->sTable[$code];
+                    $oldCode = $code;
+    
+                } else {
+    
+                    if ($code < $this->tIdx) {
+                        $string = $this->sTable[$code];
+                        $uncompData .= $string;
+    
+                        $this->addStringToTable($this->sTable[$oldCode], $string[0]);
+                        $oldCode = $code;
+                    } else {
+                        $string = $this->sTable[$oldCode];
+                        $string = $string . $string[0];
+                        $uncompData .= $string;
+    
+                        $this->addStringToTable($string);
+                        $oldCode = $code;
+                    }
+                }
+            }
+            
+            return $uncompData;
+        }
+    
+    
+        /**
+         * Initialize the string table.
+         */
+        function initsTable() {
+            $this->sTable = array();
+    
+            for ($i = 0; $i < 256; $i++)
+                $this->sTable[$i] = chr($i);
+    
+            $this->tIdx = 258;
+            $this->bitsToGet = 9;
+        }
+    
+        /**
+         * Add a new string to the string table.
+         */
+        function addStringToTable ($oldString, $newString='') {
+            $string = $oldString.$newString;
+    
+            // Add this new String to the table
+            $this->sTable[$this->tIdx++] = $string;
+    
+            if ($this->tIdx == 511) {
+                $this->bitsToGet = 10;
+            } else if ($this->tIdx == 1023) {
+                $this->bitsToGet = 11;
+            } else if ($this->tIdx == 2047) {
+                $this->bitsToGet = 12;
+            }
+        }
+    
+        // Returns the next 9, 10, 11 or 12 bits
+        function getNextCode() {
+            if ($this->bytePointer == $this->dataLength) {
+                return 257;
+            }
+    
+            $this->nextData = ($this->nextData << 8) | (ord($this->data[$this->bytePointer++]) & 0xff);
+            $this->nextBits += 8;
+    
+            if ($this->nextBits < $this->bitsToGet) {
+                $this->nextData = ($this->nextData << 8) | (ord($this->data[$this->bytePointer++]) & 0xff);
+                $this->nextBits += 8;
+            }
+    
+            $code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet-9];
+            $this->nextBits -= $this->bitsToGet;
+    
+            return $code;
+        }
+        
+        function encode($in) {
+            $this->error("LZW encoding not implemented.");
+        }
+    }
+}
diff --git a/mod/assign/feedback/editpdf/fpdi/filters/FilterLZW_FPDI.php b/mod/assign/feedback/editpdf/fpdi/filters/FilterLZW_FPDI.php
new file mode 100644 (file)
index 0000000..decc170
--- /dev/null
@@ -0,0 +1,33 @@
+<?php\r
+//\r
+//  FPDI - Version 1.4.4\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+require_once('FilterLZW.php');\r
+\r
+class FilterLZW_FPDI extends FilterLZW {\r
+\r
+    var $fpdi;\r
+\r
+    function FilterLZW_FPDI(&$fpdi) {\r
+        $this->fpdi =& $fpdi;\r
+    }\r
+    \r
+    function error($msg) {\r
+        $this->fpdi->error($msg);\r
+    }\r
+}
\ No newline at end of file
diff --git a/mod/assign/feedback/editpdf/fpdi/fpdf_tpl.php b/mod/assign/feedback/editpdf/fpdi/fpdf_tpl.php
new file mode 100644 (file)
index 0000000..cdbff35
--- /dev/null
@@ -0,0 +1,460 @@
+<?php\r
+//\r
+//  FPDF_TPL - Version 1.2.3\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+class FPDF_TPL extends FPDF {\r
+    /**\r
+     * Array of Tpl-Data\r
+     * @var array\r
+     */\r
+    var $tpls = array();\r
+\r
+    /**\r
+     * Current Template-ID\r
+     * @var int\r
+     */\r
+    var $tpl = 0;\r
+    \r
+    /**\r
+     * "In Template"-Flag\r
+     * @var boolean\r
+     */\r
+    var $_intpl = false;\r
+    \r
+    /**\r
+     * Nameprefix of Templates used in Resources-Dictonary\r
+     * @var string A String defining the Prefix used as Template-Object-Names. Have to beginn with an /\r
+     */\r
+    var $tplprefix = "/TPL";\r
+\r
+    /**\r
+     * Resources used By Templates and Pages\r
+     * @var array\r
+     */\r
+    var $_res = array();\r
+    \r
+    /**\r
+     * Last used Template data\r
+     *\r
+     * @var array\r
+     */\r
+    var $lastUsedTemplateData = array();\r
+    \r
+    /**\r
+     * Start a Template\r
+     *\r
+     * This method starts a template. You can give own coordinates to build an own sized\r
+     * Template. Pay attention, that the margins are adapted to the new templatesize.\r
+     * If you want to write outside the template, for example to build a clipped Template,\r
+     * you have to set the Margins and "Cursor"-Position manual after beginTemplate-Call.\r
+     *\r
+     * If no parameter is given, the template uses the current page-size.\r
+     * The Method returns an ID of the current Template. This ID is used later for using this template.\r
+     * Warning: A created Template is used in PDF at all events. Still if you don't use it after creation!\r
+     *\r
+     * @param int $x The x-coordinate given in user-unit\r
+     * @param int $y The y-coordinate given in user-unit\r
+     * @param int $w The width given in user-unit\r
+     * @param int $h The height given in user-unit\r
+     * @return int The ID of new created Template\r
+     */\r
+    function beginTemplate($x = null, $y = null, $w = null, $h = null) {\r
+       if (is_subclass_of($this, 'TCPDF')) {\r
+               $this->Error('This method is only usable with FPDF. Use TCPDF methods startTemplate() instead.');\r
+               return;\r
+       }\r
+       \r
+        if ($this->page <= 0)\r
+            $this->error("You have to add a page to fpdf first!");\r
+\r
+        if ($x == null)\r
+            $x = 0;\r
+        if ($y == null)\r
+            $y = 0;\r
+        if ($w == null)\r
+            $w = $this->w;\r
+        if ($h == null)\r
+            $h = $this->h;\r
+\r
+        // Save settings\r
+        $this->tpl++;\r
+        $tpl =& $this->tpls[$this->tpl];\r
+        $tpl = array(\r
+            'o_x' => $this->x,\r
+            'o_y' => $this->y,\r
+            'o_AutoPageBreak' => $this->AutoPageBreak,\r
+            'o_bMargin' => $this->bMargin,\r
+            'o_tMargin' => $this->tMargin,\r
+            'o_lMargin' => $this->lMargin,\r
+            'o_rMargin' => $this->rMargin,\r
+            'o_h' => $this->h,\r
+            'o_w' => $this->w,\r
+            'o_FontFamily' => $this->FontFamily,\r
+            'o_FontStyle' => $this->FontStyle,\r
+            'o_FontSizePt' => $this->FontSizePt,\r
+            'o_FontSize' => $this->FontSize,\r
+            'buffer' => '',\r
+            'x' => $x,\r
+            'y' => $y,\r
+            'w' => $w,\r
+            'h' => $h\r
+        );\r
+\r
+        $this->SetAutoPageBreak(false);\r
+        \r
+        // Define own high and width to calculate possitions correct\r
+        $this->h = $h;\r
+        $this->w = $w;\r
+\r
+        $this->_intpl = true;\r
+        $this->SetXY($x + $this->lMargin, $y + $this->tMargin);\r
+        $this->SetRightMargin($this->w - $w + $this->rMargin);\r
+\r
+        if ($this->CurrentFont) {\r
+            $fontkey = $this->FontFamily . $this->FontStyle;\r
+                   $this->_res['tpl'][$this->tpl]['fonts'][$fontkey] =& $this->fonts[$fontkey];\r
+            \r
+               $this->_out(sprintf('BT /F%d %.2f Tf ET', $this->CurrentFont['i'], $this->FontSizePt));\r
+        }\r
+        \r
+        return $this->tpl;\r
+    }\r
+    \r
+    /**\r
+     * End Template\r
+     *\r
+     * This method ends a template and reset initiated variables on beginTemplate.\r
+     *\r
+     * @return mixed If a template is opened, the ID is returned. If not a false is returned.\r
+     */\r
+    function endTemplate() {\r
+       if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+               return call_user_func_array(array($this, 'TCPDF::endTemplate'), $args);\r
+        }\r
+        \r
+        if ($this->_intpl) {\r
+            $this->_intpl = false; \r
+            $tpl =& $this->tpls[$this->tpl];\r
+            $this->SetXY($tpl['o_x'], $tpl['o_y']);\r
+            $this->tMargin = $tpl['o_tMargin'];\r
+            $this->lMargin = $tpl['o_lMargin'];\r
+            $this->rMargin = $tpl['o_rMargin'];\r
+            $this->h = $tpl['o_h'];\r
+            $this->w = $tpl['o_w'];\r
+            $this->SetAutoPageBreak($tpl['o_AutoPageBreak'], $tpl['o_bMargin']);\r
+            \r
+            $this->FontFamily = $tpl['o_FontFamily'];\r
+                       $this->FontStyle = $tpl['o_FontStyle'];\r
+                       $this->FontSizePt = $tpl['o_FontSizePt'];\r
+                       $this->FontSize = $tpl['o_FontSize'];\r
+               \r
+                       $fontkey = $this->FontFamily . $this->FontStyle;\r
+                       if ($fontkey)\r
+               $this->CurrentFont =& $this->fonts[$fontkey];\r
+            \r
+            return $this->tpl;\r
+        } else {\r
+            return false;\r
+        }\r
+    }\r
+    \r
+    /**\r
+     * Use a Template in current Page or other Template\r
+     *\r
+     * You can use a template in a page or in another template.\r
+     * You can give the used template a new size like you use the Image()-method.\r
+     * All parameters are optional. The width or height is calculated automaticaly\r
+     * if one is given. If no parameter is given the origin size as defined in\r
+     * beginTemplate() is used.\r
+     * The calculated or used width and height are returned as an array.\r
+     *\r
+     * @param int $tplidx A valid template-Id\r
+     * @param int $_x The x-position\r
+     * @param int $_y The y-position\r
+     * @param int $_w The new width of the template\r
+     * @param int $_h The new height of the template\r
+     * @retrun array The height and width of the template\r
+     */\r
+    function useTemplate($tplidx, $_x = null, $_y = null, $_w = 0, $_h = 0) {\r
+        if ($this->page <= 0)\r
+               $this->error('You have to add a page first!');\r
+        \r
+        if (!isset($this->tpls[$tplidx]))\r
+            $this->error('Template does not exist!');\r
+            \r
+        if ($this->_intpl) {\r
+            $this->_res['tpl'][$this->tpl]['tpls'][$tplidx] =& $this->tpls[$tplidx];\r
+        }\r
+        \r
+        $tpl =& $this->tpls[$tplidx];\r
+        $w = $tpl['w'];\r
+        $h = $tpl['h'];\r
+        \r
+        if ($_x == null)\r
+            $_x = 0;\r
+        if ($_y == null)\r
+            $_y = 0;\r
+            \r
+        $_x += $tpl['x'];\r
+        $_y += $tpl['y'];\r
+        \r
+        $wh = $this->getTemplateSize($tplidx, $_w, $_h);\r
+        $_w = $wh['w'];\r
+        $_h = $wh['h'];\r
+    \r
+        $tData = array(\r
+            'x' => $this->x,\r
+            'y' => $this->y,\r
+            'w' => $_w,\r
+            'h' => $_h,\r
+            'scaleX' => ($_w / $w),\r
+            'scaleY' => ($_h / $h),\r
+            'tx' => $_x,\r
+            'ty' =>  ($this->h - $_y - $_h),\r
+            'lty' => ($this->h - $_y - $_h) - ($this->h - $h) * ($_h / $h)\r
+        );\r
+        \r
+        $this->_out(sprintf('q %.4F 0 0 %.4F %.4F %.4F cm', $tData['scaleX'], $tData['scaleY'], $tData['tx'] * $this->k, $tData['ty'] * $this->k)); // Translate \r
+        $this->_out(sprintf('%s%d Do Q', $this->tplprefix, $tplidx));\r
+\r
+        $this->lastUsedTemplateData = $tData;\r
+        \r
+        return array('w' => $_w, 'h' => $_h);\r
+    }\r
+    \r
+    /**\r
+     * Get The calculated Size of a Template\r
+     *\r
+     * If one size is given, this method calculates the other one.\r
+     *\r
+     * @param int $tplidx A valid template-Id\r
+     * @param int $_w The width of the template\r
+     * @param int $_h The height of the template\r
+     * @return array The height and width of the template\r
+     */\r
+    function getTemplateSize($tplidx, $_w = 0, $_h = 0) {\r
+        if (!isset($this->tpls[$tplidx]))\r
+            return false;\r
+\r
+        $tpl =& $this->tpls[$tplidx];\r
+        $w = $tpl['w'];\r
+        $h = $tpl['h'];\r
+        \r
+        if ($_w == 0 and $_h == 0) {\r
+            $_w = $w;\r
+            $_h = $h;\r
+        }\r
+\r
+       if($_w == 0)\r
+               $_w = $_h * $w / $h;\r
+       if($_h == 0)\r
+               $_h = $_w * $h / $w;\r
+               \r
+        return array("w" => $_w, "h" => $_h);\r
+    }\r
+    \r
+    /**\r
+     * See FPDF/TCPDF-Documentation ;-)\r
+     */\r
+    public function SetFont($family, $style='', $size=null, $fontfile='', $subset='default', $out=true) {\r
+        if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+               return call_user_func_array(array($this, 'TCPDF::SetFont'), $args);\r
+        }\r
+        \r
+        parent::SetFont($family, $style, $size);\r
+       \r
+        $fontkey = $this->FontFamily . $this->FontStyle;\r
+        \r
+        if ($this->_intpl) {\r
+            $this->_res['tpl'][$this->tpl]['fonts'][$fontkey] =& $this->fonts[$fontkey];\r
+        } else {\r
+            $this->_res['page'][$this->page]['fonts'][$fontkey] =& $this->fonts[$fontkey];\r
+        }\r
+    }\r
+    \r
+    /**\r
+     * See FPDF/TCPDF-Documentation ;-)\r
+     */\r
+    function Image(\r
+               $file, $x = '', $y = '', $w = 0, $h = 0, $type = '', $link = '', $align = '', $resize = false,\r
+               $dpi = 300, $palign = '', $ismask = false, $imgmask = false, $border = 0, $fitbox = false,\r
+               $hidden = false, $fitonpage = false, $alt = false, $altimgs = array()\r
+    ) {\r
+        if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+                       return call_user_func_array(array($this, 'TCPDF::Image'), $args);\r
+        }\r
+        \r
+        $ret = parent::Image($file, $x, $y, $w, $h, $type, $link);\r
+        if ($this->_intpl) {\r
+            $this->_res['tpl'][$this->tpl]['images'][$file] =& $this->images[$file];\r
+        } else {\r
+            $this->_res['page'][$this->page]['images'][$file] =& $this->images[$file];\r
+        }\r
+        \r
+        return $ret;\r
+    }\r
+    \r
+    /**\r
+     * See FPDF-Documentation ;-)\r
+     *\r
+     * AddPage is not available when you're "in" a template.\r
+     */\r
+    function AddPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false) {\r
+       if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+               return call_user_func_array(array($this, 'TCPDF::AddPage'), $args);\r
+        }\r
+        \r
+        if ($this->_intpl)\r
+            $this->Error('Adding pages in templates isn\'t possible!');\r
+            \r
+        parent::AddPage($orientation, $format);\r
+    }\r
+\r
+    /**\r
+     * Preserve adding Links in Templates ...won't work\r
+     */\r
+    function Link($x, $y, $w, $h, $link, $spaces = 0) {\r
+        if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+                       return call_user_func_array(array($this, 'TCPDF::Link'), $args);\r
+        }\r
+        \r
+        if ($this->_intpl)\r
+            $this->Error('Using links in templates aren\'t possible!');\r
+            \r
+        parent::Link($x, $y, $w, $h, $link);\r
+    }\r
+    \r
+    function AddLink() {\r
+       if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+                       return call_user_func_array(array($this, 'TCPDF::AddLink'), $args);\r
+        }\r
+        \r
+        if ($this->_intpl)\r
+            $this->Error('Adding links in templates aren\'t possible!');\r
+        return parent::AddLink();\r
+    }\r
+    \r
+    function SetLink($link, $y = 0, $page = -1) {\r
+       if (is_subclass_of($this, 'TCPDF')) {\r
+               $args = func_get_args();\r
+                       return call_user_func_array(array($this, 'TCPDF::SetLink'), $args);\r
+        }\r
+        \r
+        if ($this->_intpl)\r
+            $this->Error('Setting links in templates aren\'t possible!');\r
+        parent::SetLink($link, $y, $page);\r
+    }\r
+    \r
+    /**\r
+     * Private Method that writes the form xobjects\r
+     */\r
+    function _putformxobjects() {\r
+        $filter=($this->compress) ? '/Filter /FlateDecode ' : '';\r
+           reset($this->tpls);\r
+        foreach($this->tpls AS $tplidx => $tpl) {\r
+\r
+            $p=($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer'];\r
+               $this->_newobj();\r
+               $this->tpls[$tplidx]['n'] = $this->n;\r
+               $this->_out('<<'.$filter.'/Type /XObject');\r
+            $this->_out('/Subtype /Form');\r
+            $this->_out('/FormType 1');\r
+            $this->_out(sprintf('/BBox [%.2F %.2F %.2F %.2F]',\r
+                // llx\r
+                $tpl['x'] * $this->k,\r
+                // lly\r
+                -$tpl['y'] * $this->k,\r
+                // urx\r
+                ($tpl['w'] + $tpl['x']) * $this->k,\r
+                // ury\r
+                ($tpl['h'] - $tpl['y']) * $this->k\r
+            ));\r
+            \r
+            if ($tpl['x'] != 0 || $tpl['y'] != 0) {\r
+                $this->_out(sprintf('/Matrix [1 0 0 1 %.5F %.5F]',\r
+                     -$tpl['x'] * $this->k * 2, $tpl['y'] * $this->k * 2\r
+                ));\r
+            }\r
+            \r
+            $this->_out('/Resources ');\r
+\r
+            $this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');\r
+               if (isset($this->_res['tpl'][$tplidx]['fonts']) && count($this->_res['tpl'][$tplidx]['fonts'])) {\r
+               $this->_out('/Font <<');\r
+                foreach($this->_res['tpl'][$tplidx]['fonts'] as $font)\r
+                       $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');\r
+               $this->_out('>>');\r
+            }\r
+               if(isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images']) || \r
+                  isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls']))\r
+               {\r
+                $this->_out('/XObject <<');\r
+                if (isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images'])) {\r
+                    foreach($this->_res['tpl'][$tplidx]['images'] as $image)\r
+                               $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');\r
+                }\r
+                if (isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) {\r
+                    foreach($this->_res['tpl'][$tplidx]['tpls'] as $i => $tpl)\r
+                        $this->_out($this->tplprefix . $i . ' ' . $tpl['n'] . ' 0 R');\r
+                }\r
+                $this->_out('>>');\r
+               }\r
+               $this->_out('>>');\r
+               \r
+               $this->_out('/Length ' . strlen($p) . ' >>');\r
+               $this->_putstream($p);\r
+               $this->_out('endobj');\r
+        }\r
+    }\r
+    \r
+    /**\r
+     * Overwritten to add _putformxobjects() after _putimages()\r
+     *\r
+     */\r
+    function _putimages() {\r
+        parent::_putimages();\r
+        $this->_putformxobjects();\r
+    }\r
+    \r
+    function _putxobjectdict() {\r
+        parent::_putxobjectdict();\r
+        \r
+        if (count($this->tpls)) {\r
+            foreach($this->tpls as $tplidx => $tpl) {\r
+                $this->_out(sprintf('%s%d %d 0 R', $this->tplprefix, $tplidx, $tpl['n']));\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Private Method\r
+     */\r
+    function _out($s) {\r
+        if ($this->state == 2 && $this->_intpl) {\r
+            $this->tpls[$this->tpl]['buffer'] .= $s . "\n";\r
+        } else {\r
+            parent::_out($s);\r
+        }\r
+    }\r
+}\r
diff --git a/mod/assign/feedback/editpdf/fpdi/fpdi.php b/mod/assign/feedback/editpdf/fpdi/fpdi.php
new file mode 100644 (file)
index 0000000..8cae46c
--- /dev/null
@@ -0,0 +1,588 @@
+<?php\r
+//\r
+//  FPDI - Version 1.4.4\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+define('FPDI_VERSION', '1.4.4');\r
+\r
+// Check for TCPDF and remap TCPDF to FPDF\r
+if (class_exists('TCPDF', false)) {\r
+    require_once('fpdi2tcpdf_bridge.php');\r
+}\r
+\r
+require_once('fpdf_tpl.php');\r
+require_once('fpdi_pdf_parser.php');\r
+\r
+\r
+class FPDI extends FPDF_TPL {\r
+    /**\r
+     * Actual filename\r
+     * @var string\r
+     */\r
+    var $current_filename;\r
+\r
+    /**\r
+     * Parser-Objects\r
+     * @var array\r
+     */\r
+    var $parsers;\r
+    \r
+    /**\r
+     * Current parser\r
+     * @var object\r
+     */\r
+    var $current_parser;\r
+    \r
+    /**\r
+     * object stack\r
+     * @var array\r
+     */\r
+    var $_obj_stack;\r
+    \r
+    /**\r
+     * done object stack\r
+     * @var array\r
+     */\r
+    var $_don_obj_stack;\r
+\r
+    /**\r
+     * Current Object Id.\r
+     * @var integer\r
+     */\r
+    var $_current_obj_id;\r
+    \r
+    /**\r
+     * The name of the last imported page box\r
+     * @var string\r
+     */\r
+    var $lastUsedPageBox;\r
+    \r
+    /**\r
+     * Cache for imported pages/template ids\r
+     * @var array\r
+     */\r
+    var $_importedPages = array();\r
+    \r
+    /**\r
+     * Set a source-file\r
+     *\r
+     * @param string $filename a valid filename\r
+     * @return int number of available pages\r
+     */\r
+    function setSourceFile($filename) {\r
+        $this->current_filename = $filename;\r
+        \r
+        if (!isset($this->parsers[$filename]))\r
+            $this->parsers[$filename] = $this->_getPdfParser($filename);\r
+        $this->current_parser =& $this->parsers[$filename];\r
+        \r
+        return $this->parsers[$filename]->getPageCount();\r
+    }\r
+    \r
+    /**\r
+     * Returns a PDF parser object\r
+     *\r
+     * @param string $filename\r
+     * @return fpdi_pdf_parser\r
+     */\r
+    function _getPdfParser($filename) {\r
+       return new fpdi_pdf_parser($filename, $this);\r
+    }\r
+    \r
+    /**\r
+     * Get the current PDF version\r
+     *\r
+     * @return string\r
+     */\r
+    function getPDFVersion() {\r
+               return $this->PDFVersion;\r
+       }\r
+    \r
+       /**\r
+     * Set the PDF version\r
+     *\r
+     * @return string\r
+     */\r
+       function setPDFVersion($version = '1.3') {\r
+               $this->PDFVersion = $version;\r
+       }\r
+       \r
+    /**\r
+     * Import a page\r
+     *\r
+     * @param int $pageno pagenumber\r
+     * @return int Index of imported page - to use with fpdf_tpl::useTemplate()\r
+     */\r
+    function importPage($pageno, $boxName = '/CropBox') {\r
+        if ($this->_intpl) {\r
+            return $this->error('Please import the desired pages before creating a new template.');\r
+        }\r
+        \r
+        $fn = $this->current_filename;\r
+        \r
+        // check if page already imported\r
+        $pageKey = $fn . '-' . ((int)$pageno) . $boxName;\r
+        if (isset($this->_importedPages[$pageKey]))\r
+            return $this->_importedPages[$pageKey];\r
+        \r
+        $parser =& $this->parsers[$fn];\r
+        $parser->setPageno($pageno);\r
+\r
+        if (!in_array($boxName, $parser->availableBoxes))\r
+            return $this->Error(sprintf('Unknown box: %s', $boxName));\r
+            \r
+        $pageboxes = $parser->getPageBoxes($pageno, $this->k);\r
+        \r
+        /**\r
+         * MediaBox\r
+         * CropBox: Default -> MediaBox\r
+         * BleedBox: Default -> CropBox\r
+         * TrimBox: Default -> CropBox\r
+         * ArtBox: Default -> CropBox\r
+         */\r
+        if (!isset($pageboxes[$boxName]) && ($boxName == '/BleedBox' || $boxName == '/TrimBox' || $boxName == '/ArtBox'))\r
+            $boxName = '/CropBox';\r
+        if (!isset($pageboxes[$boxName]) && $boxName == '/CropBox')\r
+            $boxName = '/MediaBox';\r
+        \r
+        if (!isset($pageboxes[$boxName]))\r
+            return false;\r
+            \r
+        $this->lastUsedPageBox = $boxName;\r
+        \r
+        $box = $pageboxes[$boxName];\r
+        \r
+        $this->tpl++;\r
+        $this->tpls[$this->tpl] = array();\r
+        $tpl =& $this->tpls[$this->tpl];\r
+        $tpl['parser'] =& $parser;\r
+        $tpl['resources'] = $parser->getPageResources();\r
+        $tpl['buffer'] = $parser->getContent();\r
+        $tpl['box'] = $box;\r
+        \r
+        // To build an array that can be used by PDF_TPL::useTemplate()\r
+        $this->tpls[$this->tpl] = array_merge($this->tpls[$this->tpl], $box);\r
+        \r
+        // An imported page will start at 0,0 everytime. Translation will be set in _putformxobjects()\r
+        $tpl['x'] = 0;\r
+        $tpl['y'] = 0;\r
+        \r
+        // handle rotated pages\r
+        $rotation = $parser->getPageRotation($pageno);\r
+        $tpl['_rotationAngle'] = 0;\r
+        if (isset($rotation[1]) && ($angle = $rotation[1] % 360) != 0) {\r
+               $steps = $angle / 90;\r
+                \r
+            $_w = $tpl['w'];\r
+            $_h = $tpl['h'];\r
+            $tpl['w'] = $steps % 2 == 0 ? $_w : $_h;\r
+            $tpl['h'] = $steps % 2 == 0 ? $_h : $_w;\r
+            \r
+            if ($angle < 0)\r
+               $angle += 360;\r
+            \r
+               $tpl['_rotationAngle'] = $angle * -1;\r
+        }\r
+        \r
+        $this->_importedPages[$pageKey] = $this->tpl;\r
+        \r
+        return $this->tpl;\r
+    }\r
+    \r
+    /**\r
+     * Returns the last used page box\r
+     *\r
+     * @return string\r
+     */\r
+    function getLastUsedPageBox() {\r
+        return $this->lastUsedPageBox;\r
+    }\r
+    \r
+    \r
+    function useTemplate($tplidx, $_x = null, $_y = null, $_w = 0, $_h = 0, $adjustPageSize = false) {\r
+        if ($adjustPageSize == true && is_null($_x) && is_null($_y)) {\r
+            $size = $this->getTemplateSize($tplidx, $_w, $_h);\r
+            $orientation = $size['w'] > $size['h'] ? 'L' : 'P';\r
+            $size = array($size['w'], $size['h']);\r
+            \r
+            if (is_subclass_of($this, 'TCPDF')) {\r
+               $this->setPageFormat($size, $orientation);\r
+            } else {\r
+               $size = $this->_getpagesize($size);\r
+               \r
+               if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1])\r
+                               {\r
+                                       // New size or orientation\r
+                                       if($orientation=='P')\r
+                                       {\r
+                                               $this->w = $size[0];\r
+                                               $this->h = $size[1];\r
+                                       }\r
+                                       else\r
+                                       {\r
+                                               $this->w = $size[1];\r
+                                               $this->h = $size[0];\r
+                                       }\r
+                                       $this->wPt = $this->w*$this->k;\r
+                                       $this->hPt = $this->h*$this->k;\r
+                                       $this->PageBreakTrigger = $this->h-$this->bMargin;\r
+                                       $this->CurOrientation = $orientation;\r
+                                       $this->CurPageSize = $size;\r
+                                       $this->PageSizes[$this->page] = array($this->wPt, $this->hPt);\r
+                               }\r
+            } \r
+        }\r
+        \r
+        $this->_out('q 0 J 1 w 0 j 0 G 0 g'); // reset standard values\r
+        $s = parent::useTemplate($tplidx, $_x, $_y, $_w, $_h);\r
+        $this->_out('Q');\r
+        \r
+        return $s;\r
+    }\r
+    \r
+    /**\r
+     * Private method, that rebuilds all needed objects of source files\r
+     */\r
+    function _putimportedobjects() {\r
+        if (is_array($this->parsers) && count($this->parsers) > 0) {\r
+            foreach($this->parsers AS $filename => $p) {\r
+                $this->current_parser =& $this->parsers[$filename];\r
+                if (isset($this->_obj_stack[$filename]) && is_array($this->_obj_stack[$filename])) {\r
+                    while(($n = key($this->_obj_stack[$filename])) !== null) {\r
+                        $nObj = $this->current_parser->pdf_resolve_object($this->current_parser->c, $this->_obj_stack[$filename][$n][1]);\r
+                                               \r
+                        $this->_newobj($this->_obj_stack[$filename][$n][0]);\r
+                        \r
+                        if ($nObj[0] == PDF_TYPE_STREAM) {\r
+                                                       $this->pdf_write_value($nObj);\r
+                        } else {\r
+                            $this->pdf_write_value($nObj[1]);\r
+                        }\r
+                        \r
+                        $this->_out('endobj');\r
+                        $this->_obj_stack[$filename][$n] = null; // free memory\r
+                        unset($this->_obj_stack[$filename][$n]);\r
+                        reset($this->_obj_stack[$filename]);\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+    \r
+    \r
+    /**\r
+     * Private Method that writes the form xobjects\r
+     */\r
+    function _putformxobjects() {\r
+        $filter=($this->compress) ? '/Filter /FlateDecode ' : '';\r
+           reset($this->tpls);\r
+        foreach($this->tpls AS $tplidx => $tpl) {\r
+            $p=($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer'];\r
+               $this->_newobj();\r
+               $cN = $this->n; // TCPDF/Protection: rem current "n"\r
+               \r
+               $this->tpls[$tplidx]['n'] = $this->n;\r
+               $this->_out('<<' . $filter . '/Type /XObject');\r
+            $this->_out('/Subtype /Form');\r
+            $this->_out('/FormType 1');\r
+            \r
+            $this->_out(sprintf('/BBox [%.2F %.2F %.2F %.2F]', \r
+                (isset($tpl['box']['llx']) ? $tpl['box']['llx'] : $tpl['x']) * $this->k,\r
+                (isset($tpl['box']['lly']) ? $tpl['box']['lly'] : -$tpl['y']) * $this->k,\r
+                (isset($tpl['box']['urx']) ? $tpl['box']['urx'] : $tpl['w'] + $tpl['x']) * $this->k,\r
+                (isset($tpl['box']['ury']) ? $tpl['box']['ury'] : $tpl['h'] - $tpl['y']) * $this->k\r
+            ));\r
+            \r
+            $c = 1;\r
+            $s = 0;\r
+            $tx = 0;\r
+            $ty = 0;\r
+            \r
+            if (isset($tpl['box'])) {\r
+                $tx = -$tpl['box']['llx'];\r
+                $ty = -$tpl['box']['lly']; \r
+                \r
+                if ($tpl['_rotationAngle'] <> 0) {\r
+                    $angle = $tpl['_rotationAngle'] * M_PI/180;\r
+                    $c=cos($angle);\r
+                    $s=sin($angle);\r
+                    \r
+                    switch($tpl['_rotationAngle']) {\r
+                        case -90:\r
+                           $tx = -$tpl['box']['lly'];\r
+                           $ty = $tpl['box']['urx'];\r
+                           break;\r
+                        case -180:\r
+                            $tx = $tpl['box']['urx'];\r
+                            $ty = $tpl['box']['ury'];\r
+                            break;\r
+                        case -270:\r
+                               $tx = $tpl['box']['ury'];\r
+                            $ty = -$tpl['box']['llx'];\r
+                            break;\r
+                    }\r
+                }\r
+            } elseif ($tpl['x'] != 0 || $tpl['y'] != 0) {\r
+                $tx = -$tpl['x'] * 2;\r
+                $ty = $tpl['y'] * 2;\r
+            }\r
+            \r
+            $tx *= $this->k;\r
+            $ty *= $this->k;\r
+            \r
+            if ($c != 1 || $s != 0 || $tx != 0 || $ty != 0) {\r
+                $this->_out(sprintf('/Matrix [%.5F %.5F %.5F %.5F %.5F %.5F]',\r
+                    $c, $s, -$s, $c, $tx, $ty\r
+                ));\r
+            }\r
+            \r
+            $this->_out('/Resources ');\r
+\r
+            if (isset($tpl['resources'])) {\r
+                $this->current_parser =& $tpl['parser'];\r
+                $this->pdf_write_value($tpl['resources']); // "n" will be changed\r
+            } else {\r
+                $this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');\r
+               if (isset($this->_res['tpl'][$tplidx]['fonts']) && count($this->_res['tpl'][$tplidx]['fonts'])) {\r
+                       $this->_out('/Font <<');\r
+                    foreach($this->_res['tpl'][$tplidx]['fonts'] as $font)\r
+                               $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');\r
+                       $this->_out('>>');\r
+                }\r
+               if(isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images']) || \r
+                  isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls']))\r
+               {\r
+                    $this->_out('/XObject <<');\r
+                    if (isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images'])) {\r
+                        foreach($this->_res['tpl'][$tplidx]['images'] as $image)\r
+                                       $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');\r
+                    }\r
+                    if (isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) {\r
+                        foreach($this->_res['tpl'][$tplidx]['tpls'] as $i => $tpl)\r
+                            $this->_out($this->tplprefix . $i . ' ' . $tpl['n'] . ' 0 R');\r
+                    }\r
+                    $this->_out('>>');\r
+               }\r
+               $this->_out('>>');\r
+            }\r
+            \r
+            $this->_out('/Group <</Type/Group/S/Transparency>>');\r
+            \r
+            $nN = $this->n; // TCPDF: rem new "n"\r
+            $this->n = $cN; // TCPDF: reset to current "n"\r
+            if (is_subclass_of($this, 'TCPDF')) {\r
+               $p = $this->_getrawstream($p);\r
+               $this->_out('/Length ' . strlen($p) . ' >>');\r
+               $this->_out("stream\n" . $p . "\nendstream");\r
+            } else {\r
+                   $this->_out('/Length ' . strlen($p) . ' >>');\r
+                       $this->_putstream($p);\r
+            }\r
+               $this->_out('endobj');\r
+               $this->n = $nN; // TCPDF: reset to new "n"\r
+        }\r
+        \r
+        $this->_putimportedobjects();\r
+    }\r
+\r
+    /**\r
+     * Rewritten to handle existing own defined objects\r
+     */\r
+    function _newobj($obj_id = false, $onlynewobj = false) {\r
+        if (!$obj_id) {\r
+            $obj_id = ++$this->n;\r
+        }\r
+\r
+        //Begin a new object\r
+        if (!$onlynewobj) {\r
+            $this->offsets[$obj_id] = is_subclass_of($this, 'TCPDF') ? $this->bufferlen : strlen($this->buffer);\r
+            $this->_out($obj_id . ' 0 obj');\r
+            $this->_current_obj_id = $obj_id; // for later use with encryption\r
+        }\r
+        \r
+        return $obj_id;\r
+    }\r
+\r
+    /**\r
+     * Writes a value\r
+     * Needed to rebuild the source document\r
+     *\r
+     * @param mixed $value A PDF-Value. Structure of values see cases in this method\r
+     */\r
+    function pdf_write_value(&$value)\r
+    {\r
+        if (is_subclass_of($this, 'TCPDF')) {\r
+            parent::pdf_write_value($value);\r
+        }\r
+        \r
+        switch ($value[0]) {\r
+\r
+               case PDF_TYPE_TOKEN:\r
+                $this->_straightOut($value[1] . ' ');\r
+                       break;\r
+                   case PDF_TYPE_NUMERIC:\r
+               case PDF_TYPE_REAL:\r
+                if (is_float($value[1]) && $value[1] != 0) {\r
+                           $this->_straightOut(rtrim(rtrim(sprintf('%F', $value[1]), '0'), '.') . ' ');\r
+                       } else {\r
+                               $this->_straightOut($value[1] . ' ');\r
+                       }\r
+                       break;\r
+                       \r
+               case PDF_TYPE_ARRAY:\r
+\r
+                       // An array. Output the proper\r
+                       // structure and move on.\r
+\r
+                       $this->_straightOut('[');\r
+                for ($i = 0; $i < count($value[1]); $i++) {\r
+                               $this->pdf_write_value($value[1][$i]);\r
+                       }\r
+\r
+                       $this->_out(']');\r
+                       break;\r
+\r
+               case PDF_TYPE_DICTIONARY:\r
+\r
+                       // A dictionary.\r
+                       $this->_straightOut('<<');\r
+\r
+                       reset ($value[1]);\r
+\r
+                       while (list($k, $v) = each($value[1])) {\r
+                               $this->_straightOut($k . ' ');\r
+                               $this->pdf_write_value($v);\r
+                       }\r
+\r
+                       $this->_straightOut('>>');\r
+                       break;\r
+\r
+               case PDF_TYPE_OBJREF:\r
+\r
+                       // An indirect object reference\r
+                       // Fill the object stack if needed\r
+                       $cpfn =& $this->current_parser->filename;\r
+                       \r
+                       if (!isset($this->_don_obj_stack[$cpfn][$value[1]])) {\r
+                           $this->_newobj(false, true);\r
+                           $this->_obj_stack[$cpfn][$value[1]] = array($this->n, $value);\r
+                    $this->_don_obj_stack[$cpfn][$value[1]] = array($this->n, $value); // Value is maybee obsolete!!!\r
+                }\r
+                $objid = $this->_don_obj_stack[$cpfn][$value[1]][0];\r
+\r
+                       $this->_out($objid . ' 0 R');\r
+                       break;\r
+\r
+               case PDF_TYPE_STRING:\r
+\r
+                       // A string.\r
+                $this->_straightOut('(' . $value[1] . ')');\r
+\r
+                       break;\r
+\r
+               case PDF_TYPE_STREAM:\r
+\r
+                       // A stream. First, output the\r
+                       // stream dictionary, then the\r
+                       // stream data itself.\r
+                $this->pdf_write_value($value[1]);\r
+                       $this->_out('stream');\r
+                       $this->_out($value[2][1]);\r
+                       $this->_out('endstream');\r
+                       break;\r
+                       \r
+            case PDF_TYPE_HEX:\r
+                $this->_straightOut('<' . $value[1] . '>');\r
+                break;\r
+\r
+            case PDF_TYPE_BOOLEAN:\r
+                   $this->_straightOut($value[1] ? 'true ' : 'false ');\r
+                   break;\r
+            \r
+               case PDF_TYPE_NULL:\r
+                // The null object.\r
+\r
+                       $this->_straightOut('null ');\r
+                       break;\r
+       }\r
+    }\r
+    \r
+    \r
+    /**\r
+     * Modified so not each call will add a newline to the output.\r
+     */\r
+    function _straightOut($s) {\r
+        if (!is_subclass_of($this, 'TCPDF')) {\r
+            if($this->state==2)\r
+                       $this->pages[$this->page] .= $s;\r
+               else\r
+                       $this->buffer .= $s;\r
+        } else {\r
+            if ($this->state == 2) {\r
+                               if ($this->inxobj) {\r
+                                       // we are inside an XObject template\r
+                                       $this->xobjects[$this->xobjid]['outdata'] .= $s;\r
+                               } elseif ((!$this->InFooter) AND isset($this->footerlen[$this->page]) AND ($this->footerlen[$this->page] > 0)) {\r
+                                       // puts data before page footer\r
+                                       $pagebuff = $this->getPageBuffer($this->page);\r
+                                       $page = substr($pagebuff, 0, -$this->footerlen[$this->page]);\r
+                                       $footer = substr($pagebuff, -$this->footerlen[$this->page]);\r
+                                       $this->setPageBuffer($this->page, $page.$s.$footer);\r
+                                       // update footer position\r
+                                       $this->footerpos[$this->page] += strlen($s);\r
+                               } else {\r
+                                       // set page data\r
+                                       $this->setPageBuffer($this->page, $s, true);\r
+                               }\r
+                       } elseif ($this->state > 0) {\r
+                               // set general data\r
+                               $this->setBuffer($s);\r
+                       }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * rewritten to close opened parsers\r
+     *\r
+     */\r
+    function _enddoc() {\r
+        parent::_enddoc();\r
+        $this->_closeParsers();\r
+    }\r
+    \r
+    /**\r
+     * close all files opened by parsers\r
+     */\r
+    function _closeParsers() {\r
+        if ($this->state > 2 && count($this->parsers) > 0) {\r
+               $this->cleanUp();\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+    \r
+    /**\r
+     * Removes cylced references and closes the file handles of the parser objects\r
+     */\r
+    function cleanUp() {\r
+       foreach ($this->parsers as $k => $_){\r
+               $this->parsers[$k]->cleanUp();\r
+               $this->parsers[$k] = null;\r
+               unset($this->parsers[$k]);\r
+        }\r
+    }\r
+}
\ No newline at end of file
diff --git a/mod/assign/feedback/editpdf/fpdi/fpdi2tcpdf_bridge.php b/mod/assign/feedback/editpdf/fpdi/fpdi2tcpdf_bridge.php
new file mode 100644 (file)
index 0000000..b4a6c70
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+//
+//  FPDI - Version 1.4.4
+//
+//    Copyright 2004-2013 Setasign - Jan Slabon
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+
+global $CFG;
+require_once($CFG->libdir.'/pdflib.php');
+
+/**
+ * This class is used as a bridge between TCPDF and FPDI
+ * and will create the possibility to use both FPDF and TCPDF
+ * via one FPDI version.
+ * 
+ * We'll simply remap TCPDF to FPDF again.
+ * 
+ * It'll be loaded and extended by FPDF_TPL.
+ * Modified to extend the moodle TCPDF wrapper instead.
+ */
+class FPDF extends pdf {
+    
+       function _putstream($s, $n=0) {
+               $this->_out($this->_getstream($s));
+       }
+       
+       function _getxobjectdict() {
+        $out = parent::_getxobjectdict();
+        if (count($this->tpls)) {
+            foreach($this->tpls as $tplidx => $tpl) {
+                $out .= sprintf('%s%d %d 0 R', $this->tplprefix, $tplidx, $tpl['n']);
+            }
+        }
+        
+        return $out;
+    }
+       
+    /**
+     * Encryption of imported data by FPDI
+     *
+     * @param array $value
+     */
+    function pdf_write_value(&$value) {
+        switch ($value[0]) {
+               case PDF_TYPE_STRING:
+                               if ($this->encrypted) {
+                                   $value[1] = $this->_unescape($value[1]);
+                    $value[1] = $this->_encrypt_data($this->_current_obj_id, $value[1]);
+                       $value[1] = TCPDF_STATIC::_escape($value[1]);
+                } 
+                       break;
+                       
+                       case PDF_TYPE_STREAM:
+                           if ($this->encrypted) {
+                               $value[2][1] = $this->_encrypt_data($this->_current_obj_id, $value[2][1]);
+                               $value[1][1]['/Length'] = array(
+                        PDF_TYPE_NUMERIC,
+                        strlen($value[2][1])
+                    );
+                }
+                break;
+                
+            case PDF_TYPE_HEX:
+               if ($this->encrypted) {
+                       $value[1] = $this->hex2str($value[1]);
+                       $value[1] = $this->_encrypt_data($this->_current_obj_id, $value[1]);
+                    
+                       // remake hexstring of encrypted string
+                               $value[1] = $this->str2hex($value[1]);
+                }
+                break;
+       }
+    }
+    
+    /**
+     * Unescapes a PDF string
+     *
+     * @param string $s
+     * @return string
+     */
+    function _unescape($s) {
+        $out = '';
+        for ($count = 0, $n = strlen($s); $count < $n; $count++) {
+            if ($s[$count] != '\\' || $count == $n-1) {
+                $out .= $s[$count];
+            } else {
+                switch ($s[++$count]) {
+                    case ')':
+                    case '(':
+                    case '\\':
+                        $out .= $s[$count];
+                        break;
+                    case 'f':
+                        $out .= chr(0x0C);
+                        break;
+                    case 'b':
+                        $out .= chr(0x08);
+                        break;
+                    case 't':
+                        $out .= chr(0x09);
+                        break;
+                    case 'r':
+                        $out .= chr(0x0D);
+                        break;
+                    case 'n':
+                        $out .= chr(0x0A);
+                        break;
+                    case "\r":
+                        if ($count != $n-1 && $s[$count+1] == "\n")
+                            $count++;
+                        break;
+                    case "\n":
+                        break;
+                    default:
+                        // Octal-Values
+                        if (ord($s[$count]) >= ord('0') &&
+                            ord($s[$count]) <= ord('9')) {
+                            $oct = ''. $s[$count];
+                                
+                            if (ord($s[$count+1]) >= ord('0') &&
+                                ord($s[$count+1]) <= ord('9')) {
+                                $oct .= $s[++$count];
+                                
+                                if (ord($s[$count+1]) >= ord('0') &&
+                                    ord($s[$count+1]) <= ord('9')) {
+                                    $oct .= $s[++$count];    
+                                }                            
+                            }
+                            
+                            $out .= chr(octdec($oct));
+                        } else {
+                            $out .= $s[$count];
+                        }
+                }
+            }
+        }
+        return $out;
+    }
+    
+    /**
+     * Hexadecimal to string
+     *
+     * @param string $hex
+     * @return string
+     */
+    function hex2str($hex) {
+       return pack('H*', str_replace(array("\r", "\n", ' '), '', $hex));
+    }
+    
+    /**
+     * String to hexadecimal
+     *
+     * @param string $str
+     * @return string
+     */
+    function str2hex($str) {
+        return current(unpack('H*', $str));
+    }
+}
diff --git a/mod/assign/feedback/editpdf/fpdi/fpdi_pdf_parser.php b/mod/assign/feedback/editpdf/fpdi/fpdi_pdf_parser.php
new file mode 100644 (file)
index 0000000..41a0e03
--- /dev/null
@@ -0,0 +1,423 @@
+<?php\r
+//\r
+//  FPDI - Version 1.4.4\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+require_once('pdf_parser.php');\r
+\r
+class fpdi_pdf_parser extends pdf_parser {\r
+\r
+    /**\r
+     * Pages\r
+     * Index beginns at 0\r
+     *\r
+     * @var array\r
+     */\r
+    var $pages;\r
+    \r
+    /**\r
+     * Page count\r
+     * @var integer\r
+     */\r
+    var $page_count;\r
+    \r
+    /**\r
+     * actual page number\r
+     * @var integer\r
+     */\r
+    var $pageno;\r
+    \r
+    /**\r
+     * PDF Version of imported Document\r
+     * @var string\r
+     */\r
+    var $pdfVersion;\r
+    \r
+    /**\r
+     * FPDI Reference\r
+     * @var object\r
+     */\r
+    var $fpdi;\r
+    \r
+    /**\r
+     * Available BoxTypes\r
+     *\r
+     * @var array\r
+     */\r
+    var $availableBoxes = array('/MediaBox', '/CropBox', '/BleedBox', '/TrimBox', '/ArtBox');\r
+        \r
+    /**\r
+     * Constructor\r
+     *\r
+     * @param string $filename  Source-Filename\r
+     * @param object $fpdi      Object of type fpdi\r
+     */\r
+    function fpdi_pdf_parser($filename, &$fpdi) {\r
+        $this->fpdi =& $fpdi;\r
+               \r
+        parent::pdf_parser($filename);\r
+\r
+        // resolve Pages-Dictonary\r
+        $pages = $this->pdf_resolve_object($this->c, $this->root[1][1]['/Pages']);\r
+\r
+        // Read pages\r
+        $this->read_pages($this->c, $pages, $this->pages);\r
+        \r
+        // count pages;\r
+        $this->page_count = count($this->pages);\r
+    }\r
+    \r
+    /**\r
+     * Removes reference to fpdi object and closes the file handle\r
+     */\r
+    function cleanUp() {\r
+       $this->fpdi = null;\r
+       $this->closeFile();\r
+    }\r
+    \r
+    /**\r
+     * Overwrite parent::error()\r
+     *\r
+     * @param string $msg  Error-Message\r
+     */\r
+    function error($msg) {\r
+       $this->fpdi->error($msg);       \r
+    }\r
+    \r
+    /**\r
+     * Get pagecount from sourcefile\r
+     *\r
+     * @return int\r
+     */\r
+    function getPageCount() {\r
+        return $this->page_count;\r
+    }\r
+\r
+\r
+    /**\r
+     * Set pageno\r
+     *\r
+     * @param int $pageno Pagenumber to use\r
+     */\r
+    function setPageno($pageno) {\r
+        $pageno = ((int) $pageno) - 1;\r
+\r
+        if ($pageno < 0 || $pageno >= $this->getPageCount()) {\r
+            $this->fpdi->error('Pagenumber is wrong!');\r
+        }\r
+\r
+        $this->pageno = $pageno;\r
+    }\r
+    \r
+    /**\r
+     * Get page-resources from current page\r
+     *\r
+     * @return array\r
+     */\r
+    function getPageResources() {\r
+        return $this->_getPageResources($this->pages[$this->pageno]);\r
+    }\r
+    \r
+    /**\r
+     * Get page-resources from /Page\r
+     *\r
+     * @param array $obj Array of pdf-data\r
+     */\r
+    function _getPageResources ($obj) { // $obj = /Page\r
+       $obj = $this->pdf_resolve_object($this->c, $obj);\r
+\r
+        // If the current object has a resources\r
+       // dictionary associated with it, we use\r
+       // it. Otherwise, we move back to its\r
+       // parent object.\r
+        if (isset ($obj[1][1]['/Resources'])) {\r
+               $res = $this->pdf_resolve_object($this->c, $obj[1][1]['/Resources']);\r
+               if ($res[0] == PDF_TYPE_OBJECT)\r
+                return $res[1];\r
+            return $res;\r
+       } else {\r
+               if (!isset ($obj[1][1]['/Parent'])) {\r
+                       return false;\r
+               } else {\r
+                $res = $this->_getPageResources($obj[1][1]['/Parent']);\r
+                if ($res[0] == PDF_TYPE_OBJECT)\r
+                    return $res[1];\r
+                return $res;\r
+               }\r
+       }\r
+    }\r
+\r
+\r
+    /**\r
+     * Get content of current page\r
+     *\r
+     * If more /Contents is an array, the streams are concated\r
+     *\r
+     * @return string\r
+     */\r
+    function getContent() {\r
+        $buffer = '';\r
+        \r
+        if (isset($this->pages[$this->pageno][1][1]['/Contents'])) {\r
+            $contents = $this->_getPageContent($this->pages[$this->pageno][1][1]['/Contents']);\r
+            foreach($contents AS $tmp_content) {\r
+                $buffer .= $this->_rebuildContentStream($tmp_content) . ' ';\r
+            }\r
+        }\r
+        \r
+        return $buffer;\r
+    }\r
+    \r
+    \r
+    /**\r
+     * Resolve all content-objects\r
+     *\r
+     * @param array $content_ref\r
+     * @return array\r
+     */\r
+    function _getPageContent($content_ref) {\r
+        $contents = array();\r
+        \r
+        if ($content_ref[0] == PDF_TYPE_OBJREF) {\r
+            $content = $this->pdf_resolve_object($this->c, $content_ref);\r
+            if ($content[1][0] == PDF_TYPE_ARRAY) {\r
+                $contents = $this->_getPageContent($content[1]);\r
+            } else {\r
+                $contents[] = $content;\r
+            }\r
+        } elseif ($content_ref[0] == PDF_TYPE_ARRAY) {\r
+            foreach ($content_ref[1] AS $tmp_content_ref) {\r
+                $contents = array_merge($contents,$this->_getPageContent($tmp_content_ref));\r
+            }\r
+        }\r
+\r
+        return $contents;\r
+    }\r
+\r
+\r
+    /**\r
+     * Rebuild content-streams\r
+     *\r
+     * @param array $obj\r
+     * @return string\r
+     */\r
+    function _rebuildContentStream($obj) {\r
+        $filters = array();\r
+        \r
+        if (isset($obj[1][1]['/Filter'])) {\r
+            $_filter = $obj[1][1]['/Filter'];\r
+\r
+            if ($_filter[0] == PDF_TYPE_OBJREF) {\r
+                $tmpFilter = $this->pdf_resolve_object($this->c, $_filter);\r
+                $_filter = $tmpFilter[1];\r
+            }\r
+            \r
+            if ($_filter[0] == PDF_TYPE_TOKEN) {\r
+                $filters[] = $_filter;\r
+            } elseif ($_filter[0] == PDF_TYPE_ARRAY) {\r
+                $filters = $_filter[1];\r
+            }\r
+        }\r
+\r
+        $stream = $obj[2][1];\r
+\r
+        foreach ($filters AS $_filter) {\r
+            switch ($_filter[1]) {\r
+                case '/FlateDecode':\r
+                case '/Fl':\r
+                       // $stream .= "\x0F\x0D"; // in an errorious stream this suffix could work\r
+                       // $stream .= "\x0A";\r
+                       // $stream .= "\x0D";\r
+                       if (function_exists('gzuncompress')) {\r
+                               $oStream = $stream;\r
+                        $stream = (strlen($stream) > 0) ? @gzuncompress($stream) : '';\r
+                    } else {\r
+                        $this->error(sprintf('To handle %s filter, please compile php with zlib support.',$_filter[1]));\r
+                    }\r
+                    \r
+                    if ($stream === false) {\r
+                       $oStream = substr($oStream, 2);\r
+                       $stream = @gzinflate($oStream);\r
+                       if ($stream == false) {\r
+                               $this->error('Error while decompressing stream.');\r
+                       }\r
+                    }\r
+                break;\r
+                case '/LZWDecode':\r
+                    include_once('filters/FilterLZW_FPDI.php');\r
+                    $decoder = new FilterLZW_FPDI($this->fpdi);\r
+                    $stream = $decoder->decode($stream);\r
+                    break;\r
+                case '/ASCII85Decode':\r
+                    include_once('filters/FilterASCII85_FPDI.php');\r
+                    $decoder = new FilterASCII85_FPDI($this->fpdi);\r
+                    $stream = $decoder->decode($stream);\r
+                    break;\r
+                case null:\r
+                    $stream = $stream;\r
+                break;\r
+                default:\r
+                    $this->error(sprintf('Unsupported Filter: %s',$_filter[1]));\r
+            }\r
+        }\r
+        \r
+        return $stream;\r
+    }\r
+    \r
+    \r
+    /**\r
+     * Get a Box from a page\r
+     * Arrayformat is same as used by fpdf_tpl\r
+     *\r
+     * @param array $page a /Page\r
+     * @param string $box_index Type of Box @see $availableBoxes\r
+     * @param float Scale factor from user space units to points\r
+     * @return array\r
+     */\r
+    function getPageBox($page, $box_index, $k) {\r
+        $page = $this->pdf_resolve_object($this->c, $page);\r
+        $box = null;\r
+        if (isset($page[1][1][$box_index]))\r
+            $box =& $page[1][1][$box_index];\r
+        \r
+        if (!is_null($box) && $box[0] == PDF_TYPE_OBJREF) {\r
+            $tmp_box = $this->pdf_resolve_object($this->c, $box);\r
+            $box = $tmp_box[1];\r
+        }\r
+            \r
+        if (!is_null($box) && $box[0] == PDF_TYPE_ARRAY) {\r
+            $b =& $box[1];\r
+            return array('x' => $b[0][1] / $k,\r
+                         'y' => $b[1][1] / $k,\r
+                         'w' => abs($b[0][1] - $b[2][1]) / $k,\r
+                         'h' => abs($b[1][1] - $b[3][1]) / $k,\r
+                         'llx' => min($b[0][1], $b[2][1]) / $k,\r
+                         'lly' => min($b[1][1], $b[3][1]) / $k,\r
+                         'urx' => max($b[0][1], $b[2][1]) / $k,\r
+                         'ury' => max($b[1][1], $b[3][1]) / $k,\r
+                         );\r
+        } elseif (!isset ($page[1][1]['/Parent'])) {\r
+            return false;\r
+        } else {\r
+            return $this->getPageBox($this->pdf_resolve_object($this->c, $page[1][1]['/Parent']), $box_index, $k);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Get all page boxes by page no\r
+     * \r
+     * @param int The page number\r
+     * @param float Scale factor from user space units to points\r
+     * @return array\r
+     */\r
+     function getPageBoxes($pageno, $k) {\r
+        return $this->_getPageBoxes($this->pages[$pageno - 1], $k);\r
+    }\r
+    \r
+    /**\r
+     * Get all boxes from /Page\r
+     *\r
+     * @param array a /Page\r
+     * @return array\r
+     */\r
+    function _getPageBoxes($page, $k) {\r
+        $boxes = array();\r
+\r
+        foreach($this->availableBoxes AS $box) {\r
+            if ($_box = $this->getPageBox($page, $box, $k)) {\r
+                $boxes[$box] = $_box;\r
+            }\r
+        }\r
+\r
+        return $boxes;\r
+    }\r
+\r
+    /**\r
+     * Get the page rotation by pageno\r
+     *\r
+     * @param integer $pageno\r
+     * @return array\r
+     */\r
+    function getPageRotation($pageno) {\r
+        return $this->_getPageRotation($this->pages[$pageno - 1]);\r
+    }\r
+    \r
+    function _getPageRotation($obj) { // $obj = /Page\r
+       $obj = $this->pdf_resolve_object($this->c, $obj);\r
+       if (isset ($obj[1][1]['/Rotate'])) {\r
+               $res = $this->pdf_resolve_object($this->c, $obj[1][1]['/Rotate']);\r
+               if ($res[0] == PDF_TYPE_OBJECT)\r
+                return $res[1];\r
+            return $res;\r
+       } else {\r
+               if (!isset ($obj[1][1]['/Parent'])) {\r
+                       return false;\r
+               } else {\r
+                $res = $this->_getPageRotation($obj[1][1]['/Parent']);\r
+                if ($res[0] == PDF_TYPE_OBJECT)\r
+                    return $res[1];\r
+                return $res;\r
+               }\r
+       }\r
+    }\r
+    \r
+    /**\r
+     * Read all /Page(es)\r
+     *\r
+     * @param object pdf_context\r
+     * @param array /Pages\r
+     * @param array the result-array\r
+     */\r
+    function read_pages(&$c, &$pages, &$result) {\r
+        // Get the kids dictionary\r
+       $_kids = $this->pdf_resolve_object ($c, $pages[1][1]['/Kids']);\r
+        \r
+        if (!is_array($_kids))\r
+            $this->error('Cannot find /Kids in current /Page-Dictionary');\r
+            \r
+        if ($_kids[1][0] == PDF_TYPE_ARRAY) {\r
+            $kids = $_kids[1][1];\r
+        } else {\r
+            $kids = $_kids[1];\r
+        }\r
+        \r
+        foreach ($kids as $v) {\r
+               $pg = $this->pdf_resolve_object ($c, $v);\r
+            if ($pg[1][1]['/Type'][1] === '/Pages') {\r
+                // If one of the kids is an embedded\r
+                       // /Pages array, resolve it as well.\r
+                if ($pg !== $pages) {\r
+                    $this->read_pages($c, $pg, $result);\r
+                }\r
+               } else {\r
+                       $result[] = $pg;\r
+               }\r
+       }\r
+    }\r
+\r
+    \r
+    \r
+    /**\r
+     * Get PDF-Version\r
+     *\r
+     * And reset the PDF Version used in FPDI if needed\r
+     */\r
+    function getPDFVersion() {\r
+        parent::getPDFVersion();\r
+        $this->fpdi->setPDFVersion(max($this->fpdi->getPDFVersion(), $this->pdfVersion));\r
+    }\r
+}\r
diff --git a/mod/assign/feedback/editpdf/fpdi/pdf_context.php b/mod/assign/feedback/editpdf/fpdi/pdf_context.php
new file mode 100644 (file)
index 0000000..f404039
--- /dev/null
@@ -0,0 +1,104 @@
+<?php\r
+//\r
+//  FPDI - Version 1.4.4\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+if (!class_exists('pdf_context', false)) {\r
+    \r
+    class pdf_context {\r
+    \r
+        /**\r
+         * Modi\r
+         *\r
+         * @var integer 0 = file | 1 = string\r
+         */\r
+        var $_mode = 0;\r
+        \r
+       var $file;\r
+       var $buffer;\r
+       var $offset;\r
+       var $length;\r
+    \r
+       var $stack;\r
+    \r
+       // Constructor\r
+    \r
+       function pdf_context(&$f) {\r
+               $this->file =& $f;\r
+               if (is_string($this->file))\r
+                   $this->_mode = 1;\r
+               $this->reset();\r
+       }\r
+    \r
+       // Optionally move the file\r
+       // pointer to a new location\r
+       // and reset the buffered data\r
+    \r
+       function reset($pos = null, $l = 100) {\r
+           if ($this->_mode == 0) {\r
+               if (!is_null ($pos)) {\r
+                               fseek ($this->file, $pos);\r
+                       }\r
+        \r
+                       $this->buffer = $l > 0 ? fread($this->file, $l) : '';\r
+                       $this->length = strlen($this->buffer);\r
+                       if ($this->length < $l)\r
+                    $this->increase_length($l - $this->length);\r
+           } else {\r
+               $this->buffer = $this->file;\r
+               $this->length = strlen($this->buffer);\r
+           }\r
+               $this->offset = 0;\r
+               $this->stack = array();\r
+       }\r
+    \r
+       // Make sure that there is at least one\r
+       // character beyond the current offset in\r
+       // the buffer to prevent the tokenizer\r
+       // from attempting to access data that does\r
+       // not exist\r
+    \r
+       function ensure_content() {\r
+               if ($this->offset >= $this->length - 1) {\r
+                       return $this->increase_length();\r
+               } else {\r
+                       return true;\r
+               }\r
+       }\r
+    \r
+       // Forcefully read more data into the buffer\r
+    \r
+       function increase_length($l = 100) {\r
+                       if ($this->_mode == 0 && feof($this->file)) {\r
+                               return false;\r
+                       } elseif ($this->_mode == 0) {\r
+                           $totalLength = $this->length + $l;\r
+                           do {\r
+                               $toRead = $totalLength - $this->length;\r
+                               if ($toRead < 1)\r
+                                       break;\r
+                           \r
+                               $this->buffer .= fread($this->file, $toRead);\r
+                   } while ((($this->length = strlen($this->buffer)) != $totalLength) && !feof($this->file));\r
+                               \r
+                               return true;\r
+                       } else {\r
+                       return false;\r
+                       }\r
+               }\r
+    }\r
+}
\ No newline at end of file
diff --git a/mod/assign/feedback/editpdf/fpdi/pdf_parser.php b/mod/assign/feedback/editpdf/fpdi/pdf_parser.php
new file mode 100644 (file)
index 0000000..b1e7b8d
--- /dev/null
@@ -0,0 +1,719 @@
+<?php\r
+//\r
+//  FPDI - Version 1.4.4\r
+//\r
+//    Copyright 2004-2013 Setasign - Jan Slabon\r
+//\r
+//  Licensed under the Apache License, Version 2.0 (the "License");\r
+//  you may not use this file except in compliance with the License.\r
+//  You may obtain a copy of the License at\r
+//\r
+//      http://www.apache.org/licenses/LICENSE-2.0\r
+//\r
+//  Unless required by applicable law or agreed to in writing, software\r
+//  distributed under the License is distributed on an "AS IS" BASIS,\r
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+//  See the License for the specific language governing permissions and\r
+//  limitations under the License.\r
+//\r
+\r
+if (!defined ('PDF_TYPE_NULL'))\r
+    define ('PDF_TYPE_NULL', 0);\r
+if (!defined ('PDF_TYPE_NUMERIC'))\r
+    define ('PDF_TYPE_NUMERIC', 1);\r
+if (!defined ('PDF_TYPE_TOKEN'))\r
+    define ('PDF_TYPE_TOKEN', 2);\r
+if (!defined ('PDF_TYPE_HEX'))\r
+    define ('PDF_TYPE_HEX', 3);\r
+if (!defined ('PDF_TYPE_STRING'))\r
+    define ('PDF_TYPE_STRING', 4);\r
+if (!defined ('PDF_TYPE_DICTIONARY'))\r
+    define ('PDF_TYPE_DICTIONARY', 5);\r
+if (!defined ('PDF_TYPE_ARRAY'))\r
+    define ('PDF_TYPE_ARRAY', 6);\r
+if (!defined ('PDF_TYPE_OBJDEC'))\r
+    define ('PDF_TYPE_OBJDEC', 7);\r
+if (!defined ('PDF_TYPE_OBJREF'))\r
+    define ('PDF_TYPE_OBJREF', 8);\r
+if (!defined ('PDF_TYPE_OBJECT'))\r
+    define ('PDF_TYPE_OBJECT', 9);\r
+if (!defined ('PDF_TYPE_STREAM'))\r
+    define ('PDF_TYPE_STREAM', 10);\r
+if (!defined ('PDF_TYPE_BOOLEAN'))\r
+    define ('PDF_TYPE_BOOLEAN', 11);\r
+if (!defined ('PDF_TYPE_REAL'))\r
+    define ('PDF_TYPE_REAL', 12);\r
+    \r
+require_once('pdf_context.php');\r
+\r
+if (!class_exists('pdf_parser', false)) {\r
+    \r
+    class pdf_parser {\r
+       \r
+       /**\r
+         * Filename\r
+         * @var string\r
+         */\r
+        var $filename;\r
+        \r
+        /**\r
+         * File resource\r
+         * @var resource\r
+         */\r
+        var $f;\r
+        \r
+        /**\r
+         * PDF Context\r
+         * @var object pdf_context-Instance\r
+         */\r
+        var $c;\r
+        \r
+        /**\r
+         * xref-Data\r
+         * @var array\r
+         */\r
+        var $xref;\r
+    \r
+        /**\r
+         * root-Object\r
+         * @var array\r
+         */\r
+        var $root;\r
+       \r
+        /**\r
+         * PDF version of the loaded document\r
+         * @var string\r
+         */\r
+        var $pdfVersion;\r
+        \r
+        /**\r
+            * For reading encrypted documents and xref/objectstreams are in use\r
+            *\r
+            * @var boolean\r
+            */\r
+           var $readPlain = true;\r
+           \r
+        /**\r
+         * Constructor\r
+         *\r
+         * @param string $filename  Source-Filename\r
+         */\r
+       function pdf_parser($filename) {\r
+            $this->filename = $filename;\r
+            \r
+            $this->f = @fopen($this->filename, 'rb');\r
+    \r
+            if (!$this->f)\r
+                $this->error(sprintf('Cannot open %s !', $filename));\r
+    \r
+            $this->getPDFVersion();\r
+    \r
+            $this->c = new pdf_context($this->f);\r
+            \r
+            // Read xref-Data\r
+            $this->xref = array();\r
+            $this->pdf_read_xref($this->xref, $this->pdf_find_xref());\r
+            \r
+            // Check for Encryption\r
+            $this->getEncryption();\r
+    \r
+            // Read root\r
+            $this->pdf_read_root();\r
+        }\r
+        \r
+        /**\r
+         * Close the opened file\r
+         */\r
+        function closeFile() {\r
+               if (isset($this->f) && is_resource($this->f)) {\r
+                   fclose($this->f);   \r
+                       unset($this->f);\r
+               }       \r
+        }\r
+        \r
+        /**\r
+         * Print Error and die\r
+         *\r
+         * @param string $msg  Error-Message\r
+         */\r
+        function error($msg) {\r
+               die('<b>PDF-Parser Error:</b> ' . $msg);        \r
+        }\r
+        \r
+        /**\r
+         * Check Trailer for Encryption\r
+         */\r
+        function getEncryption() {\r
+            if (isset($this->xref['trailer'][1]['/Encrypt'])) {\r
+               $this->error('File is encrypted!');\r
+            }\r
+        }\r
+        \r
+       /**\r
+         * Find/Return /Root\r
+         *\r
+         * @return array\r
+         */\r
+        function pdf_find_root() {\r
+            if ($this->xref['trailer'][1]['/Root'][0] != PDF_TYPE_OBJREF) {\r
+                $this->error('Wrong Type of Root-Element! Must be an indirect reference');\r
+            }\r
+            \r
+            return $this->xref['trailer'][1]['/Root'];\r
+        }\r
+    \r
+        /**\r
+         * Read the /Root\r
+         */\r
+        function pdf_read_root() {\r
+            // read root\r
+            $this->root = $this->pdf_resolve_object($this->c, $this->pdf_find_root());\r
+        }\r
+        \r
+        /**\r
+         * Get PDF-Version\r
+         *\r
+         * And reset the PDF Version used in FPDI if needed\r
+         */\r
+        function getPDFVersion() {\r
+            fseek($this->f, 0);\r
+            preg_match('/\d\.\d/',fread($this->f, 16), $m);\r
+            if (isset($m[0]))\r
+                $this->pdfVersion = $m[0];\r
+            return $this->pdfVersion;\r
+        }\r
+        \r
+        /**\r
+         * Find the xref-Table\r
+         */\r
+        function pdf_find_xref() {\r
+               $toRead = 1500;\r
+                    \r
+            $stat = fseek ($this->f, -$toRead, SEEK_END);\r
+            if ($stat === -1) {\r
+                fseek ($this->f, 0);\r
+            }\r
+               $data = fread($this->f, $toRead);\r
+            \r
+            $pos = strlen($data) - strpos(strrev($data), strrev('startxref')); \r
+            $data = substr($data, $pos);\r
+            \r
+            if (!preg_match('/\s*(\d+).*$/s', $data, $matches)) {\r
+                $this->error('Unable to find pointer to xref table');\r
+               }\r
+    \r
+               return (int) $matches[1];\r
+        }\r
+    \r
+        /**\r
+         * Read xref-table\r
+         *\r
+         * @param array $result Array of xref-table\r
+         * @param integer $offset of xref-table\r
+         */\r
+        function pdf_read_xref(&$result, $offset) {\r
+            $o_pos = $offset-min(20, $offset);\r
+               fseek($this->f, $o_pos); // set some bytes backwards to fetch errorious docs\r
+                \r
+            $data = fread($this->f, 100);\r
+            \r
+            $xrefPos = strrpos($data, 'xref');\r
+    \r
+            if ($xrefPos === false) {\r
+                fseek($this->f, $offset);\r
+                $c = new pdf_context($this->f);\r
+                $xrefStreamObjDec = $this->pdf_read_value($c);\r
+                \r
+                if (is_array($xrefStreamObjDec) && isset($xrefStreamObjDec[0]) && $xrefStreamObjDec[0] == PDF_TYPE_OBJDEC) {\r
+                    $this->error(sprintf('This document (%s) probably uses a compression technique which is not supported by the free parser shipped with FPDI.', $this->filename));\r
+                } else {            \r
+                       $this->error('Unable to find xref table.');\r
+                }\r
+            }\r
+            \r
+            if (!isset($result['xref_location'])) {\r
+                $result['xref_location'] = $o_pos + $xrefPos;\r
+                $result['max_object'] = 0;\r
+               }\r
+    \r
+               $cylces = -1;\r
+            $bytesPerCycle = 100;\r
+            \r
+               fseek($this->f, $o_pos = $o_pos + $xrefPos + 4); // set the handle directly after the "xref"-keyword\r
+            $data = fread($this->f, $bytesPerCycle);\r
+            \r
+            while (($trailerPos = strpos($data, 'trailer', max($bytesPerCycle * $cylces++, 0))) === false && !feof($this->f)) {\r
+                $data .= fread($this->f, $bytesPerCycle);\r
+            }\r
+            \r
+            if ($trailerPos === false) {\r
+                $this->error('Trailer keyword not found after xref table');\r
+            }\r
+            \r
+            $data = substr($data, 0, $trailerPos);\r
+            \r
+            // get Line-Ending\r
+            preg_match_all("/(\r\n|\n|\r)/", substr($data, 0, 100), $m); // check the first 100 bytes for linebreaks\r
+    \r
+            $differentLineEndings = count(array_unique($m[0]));\r
+            if ($differentLineEndings > 1) {\r
+                $lines = preg_split("/(\r\n|\n|\r)/", $data, -1, PREG_SPLIT_NO_EMPTY);\r
+            } else {\r
+                $lines = explode($m[0][1], $data);\r
+            }\r
+            \r
+            $data = $differentLineEndings = $m = null;\r
+            unset($data, $differentLineEndings, $m);\r
+            \r
+            $linesCount = count($lines);\r
+            \r
+            $start = 1;\r
+            \r
+            for ($i = 0; $i < $linesCount; $i++) {\r
+                $line = trim($lines[$i]);\r
+                if ($line) {\r
+                    $pieces = explode(' ', $line);\r
+                    $c = count($pieces);\r
+                    switch($c) {\r
+                        case 2:\r
+                            $start = (int)$pieces[0];\r
+                            $end   = $start + (int)$pieces[1];\r
+                            if ($end > $result['max_object'])\r
+                                $result['max_object'] = $end;\r
+                            break;\r
+                        case 3:\r
+                            if (!isset($result['xref'][$start]))\r
+                                $result['xref'][$start] = array();\r
+                            \r
+                            if (!array_key_exists($gen = (int) $pieces[1], $result['xref'][$start])) {\r
+                               $result['xref'][$start][$gen] = $pieces[2] == 'n' ? (int) $pieces[0] : null;\r
+                           }\r
+                            $start++;\r
+                            break;\r
+                        default:\r
+                            $this->error('Unexpected data in xref table');\r
+                    }\r
+                }\r
+            }\r
+            \r
+            $lines = $pieces = $line = $start = $end = $gen = null;\r
+            unset($lines, $pieces, $line, $start, $end, $gen);\r
+            \r
+            fseek($this->f, $o_pos + $trailerPos + 7);\r
+            \r
+            $c = new pdf_context($this->f);\r
+           $trailer = $this->pdf_read_value($c);\r
+           \r
+           $c = null;\r
+           unset($c);\r
+           \r
+           if (!isset($result['trailer'])) {\r
+                $result['trailer'] = $trailer;          \r
+           }\r
+           \r
+           if (isset($trailer[1]['/Prev'])) {\r
+               $this->pdf_read_xref($result, $trailer[1]['/Prev'][1]);\r
+           } \r
+           \r
+           $trailer = null;\r
+           unset($trailer);\r
+            \r
+            return true;\r
+        }\r
+        \r
+        /**\r
+         * Reads an Value\r
+         *\r
+         * @param object $c pdf_context\r
+         * @param string $token a Token\r
+         * @return mixed\r
+         */\r
+        function pdf_read_value(&$c, $token = null) {\r
+               if (is_null($token)) {\r
+                   $token = $this->pdf_read_token($c);\r
+               }\r
+               \r
+            if ($token === false) {\r
+                   return false;\r
+               }\r
+    \r
+               switch ($token) {\r
+                case   '<':\r
+                               // This is a hex string.\r
+                               // Read the value, then the terminator\r
+    \r
+                    $pos = $c->offset;\r
+    \r
+                               while(1) {\r
+    \r
+                        $match = strpos ($c->buffer, '>', $pos);\r
+                               \r
+                                       // If you can't find it, try\r
+                                       // reading more data from the stream\r
+    \r
+                                       if ($match === false) {\r
+                                               if (!$c->increase_length()) {\r
+                                                       return false;\r
+                                               } else {\r
+                               continue;\r
+                               }\r
+                                       }\r
+    \r
+                                       $result = substr ($c->buffer, $c->offset, $match - $c->offset);\r
+                                       $c->offset = $match + 1;\r
+                                       \r
+                                       return array (PDF_TYPE_HEX, $result);\r
+                    }\r
+                    \r
+                    break;\r
+                       case    '<<':\r
+                               // This is a dictionary.\r
+    \r
+                               $result = array();\r
+    \r
+                               // Recurse into this function until we reach\r
+                               // the end of the dictionary.\r
+                               while (($key = $this->pdf_read_token($c)) !== '>>') {\r
+                                       if ($key === false) {\r
+                                               return false;\r
+                                       }\r
+                                       \r
+                                       if (($value =   $this->pdf_read_value($c)) === false) {\r
+                                               return false;\r
+                                       }\r
+                                       \r
+                                       // Catch missing value\r
+                                       if ($value[0] == PDF_TYPE_TOKEN && $value[1] == '>>') {\r
+                                           $result[$key] = array(PDF_TYPE_NULL);\r
+                                           break;\r
+                                       }\r
+                                       \r
+                                       $result[$key] = $value;\r
+                               }\r
+                               \r
+                               return array (PDF_TYPE_DICTIONARY, $result);\r
+    \r
+                       case    '[':\r
+                               // This is an array.\r
+    \r
+                               $result = array();\r
+    \r
+                               // Recurse into this function until we reach\r
+                               // the end of the array.\r
+                               while (($token = $this->pdf_read_token($c)) !== ']') {\r
+                        if ($token === false) {\r
+                                               return false;\r
+                                       }\r
+                                       \r
+                                       if (($value = $this->pdf_read_value($c, $token)) === false) {\r
+                            return false;\r
+                                       }\r
+                                       \r
+                                       $result[] = $value;\r
+                               }\r
+                               \r
+                    return array (PDF_TYPE_ARRAY, $result);\r
+    \r
+                       case    '('             :\r
+                    // This is a string\r
+                    $pos = $c->offset;\r
+                    \r
+                    $openBrackets = 1;\r
+                               do {\r
+                        for (; $openBrackets != 0 && $pos < $c->length; $pos++) {\r
+                            switch (ord($c->buffer[$pos])) {\r
+                                case 0x28: // '('\r
+                                    $openBrackets++;\r
+                                    break;\r
+                                case 0x29: // ')'\r
+                                    $openBrackets--;\r
+                                    break;\r
+                                case 0x5C: // backslash\r
+                                    $pos++;\r
+                            }\r
+                        }\r
+                               } while($openBrackets != 0 && $c->increase_length());\r
+                               \r
+                               $result = substr($c->buffer, $c->offset, $pos - $c->offset - 1);\r
+                               $c->offset = $pos;\r
+                               \r
+                               return array (PDF_TYPE_STRING, $result);\r
+    \r
+                case 'stream':\r
+                       $o_pos = ftell($c->file)-strlen($c->buffer);\r
+                       $o_offset = $c->offset;\r
+                       \r
+                       $c->reset($startpos = $o_pos + $o_offset);\r
+                       \r
+                       $e = 0; // ensure line breaks in front of the stream\r
+                       if ($c->buffer[0] == chr(10) || $c->buffer[0] == chr(13))\r
+                               $e++;\r
+                       if ($c->buffer[1] == chr(10) && $c->buffer[0] != chr(10))\r
+                               $e++;\r
+                       \r
+                       if ($this->actual_obj[1][1]['/Length'][0] == PDF_TYPE_OBJREF) {\r
+                               $tmp_c = new pdf_context($this->f);\r
+                               $tmp_length = $this->pdf_resolve_object($tmp_c, $this->actual_obj[1][1]['/Length']);\r
+                               $length = $tmp_length[1][1];\r
+                       } else {\r
+                               $length = $this->actual_obj[1][1]['/Length'][1];        \r
+                       }\r
+                               \r
+                       if ($length > 0) {\r
+                               $c->reset($startpos + $e,$length);\r
+                               $v = $c->buffer;\r
+                       } else {\r
+                           $v = '';   \r
+                       }\r
+                       $c->reset($startpos + $e + $length + 9); // 9 = strlen("endstream")\r
+                       \r
+                       return array(PDF_TYPE_STREAM, $v);\r
+                       \r
+               default :\r
+                       if (is_numeric ($token)) {\r
+                        // A numeric token. Make sure that\r
+                                       // it is not part of something else.\r
+                                       if (($tok2 = $this->pdf_read_token ($c)) !== false) {\r
+                            if (is_numeric ($tok2)) {\r
+    \r
+                                                       // Two numeric tokens in a row.\r
+                                                       // In this case, we're probably in\r
+                                                       // front of either an object reference\r
+                                                       // or an object specification.\r
+                                                       // Determine the case and return the data\r
+                                                       if (($tok3 = $this->pdf_read_token ($c)) !== false) {\r
+                                    switch ($tok3) {\r
+                                                                       case 'obj':\r
+                                            return array (PDF_TYPE_OBJDEC, (int) $token, (int) $tok2);\r
+                                                                       case 'R':\r
+                                                                               return array (PDF_TYPE_OBJREF, (int) $token, (int) $tok2);\r
+                                                               }\r
+                                                               // If we get to this point, that numeric value up\r
+                                                               // there was just a numeric value. Push the extra\r
+                                                               // tokens back into the stack and return the value.\r
+                                                               array_push ($c->stack, $tok3);\r
+                                                       }\r
+                                               }\r
+    \r
+                                               array_push ($c->stack, $tok2);\r
+                                       }\r
+    \r
+                                       if ($token === (string)((int)$token))\r
+                                       return array (PDF_TYPE_NUMERIC, (int)$token);\r
+                                       else \r
+                                               return array (PDF_TYPE_REAL, (float)$token);\r
+                               } elseif ($token == 'true' || $token == 'false') {\r
+                        return array (PDF_TYPE_BOOLEAN, $token == 'true');\r
+                               } elseif ($token == 'null') {\r
+                                  return array (PDF_TYPE_NULL);\r
+                               } else {\r
+                        // Just a token. Return it.\r
+                                       return array (PDF_TYPE_TOKEN, $token);\r
+                               }\r
+             }\r
+        }\r
+        \r
+        /**\r
+         * Resolve an object\r
+         *\r
+         * @param object $c pdf_context\r
+         * @param array $obj_spec The object-data\r
+         * @param boolean $encapsulate Must set to true, cause the parsing and fpdi use this method only without this para\r
+         */\r
+        function pdf_resolve_object(&$c, $obj_spec, $encapsulate = true) {\r
+            // Exit if we get invalid data\r
+               if (!is_array($obj_spec)) {\r
+                $ret = false;\r
+                   return $ret;\r
+               }\r
+    \r
+               if ($obj_spec[0] == PDF_TYPE_OBJREF) {\r
+    \r
+                       // This is a reference, resolve it\r
+                       if (isset($this->xref['xref'][$obj_spec[1]][$obj_spec[2]])) {\r
+    \r
+                               // Save current file position\r
+                               // This is needed if you want to resolve\r
+                               // references while you're reading another object\r
+                               // (e.g.: if you need to determine the length\r
+                               // of a stream)\r
+    \r
+                               $old_pos = ftell($c->file);\r
+    \r
+                               // Reposition the file pointer and\r
+                               // load the object header.\r
+                               \r
+                               $c->reset($this->xref['xref'][$obj_spec[1]][$obj_spec[2]]);\r
+    \r
+                               $header = $this->pdf_read_value($c);\r
+    \r
+                               if ($header[0] != PDF_TYPE_OBJDEC || $header[1] != $obj_spec[1] || $header[2] != $obj_spec[2]) {\r
+                                       $toSearchFor = $obj_spec[1] . ' ' . $obj_spec[2] . ' obj';\r
+                                       if (preg_match('/' . $toSearchFor . '/', $c->buffer)) {\r
+                                               $c->offset = strpos($c->buffer, $toSearchFor) + strlen($toSearchFor);\r
+                                               // reset stack\r
+                                               $c->stack = array();\r
+                                       } else {\r
+                                               $this->error("Unable to find object ({$obj_spec[1]}, {$obj_spec[2]}) at expected location");\r
+                                       }\r
+                               }\r
+    \r
+                               // If we're being asked to store all the information\r
+                               // about the object, we add the object ID and generation\r
+                               // number for later use\r
+                               $result = array();\r
+                               $this->actual_obj =& $result;\r
+                               if ($encapsulate) {\r
+                                       $result = array (\r
+                                               PDF_TYPE_OBJECT,\r
+                                               'obj' => $obj_spec[1],\r
+                                               'gen' => $obj_spec[2]\r
+                                       );\r
+                               } \r
+    \r
+                               // Now simply read the object data until\r
+                               // we encounter an end-of-object marker\r
+                               while(1) {\r
+                        $value = $this->pdf_read_value($c);\r
+                                       if ($value === false || count($result) > 4) {\r
+                                               // in this case the parser coudn't find an endobj so we break here\r
+                                               break;\r
+                                       }\r
+    \r
+                                       if ($value[0] == PDF_TYPE_TOKEN && $value[1] === 'endobj') {\r
+                                               break;\r
+                                       }\r
+    \r
+                        $result[] = $value;\r
+                               }\r
+    \r
+                               $c->reset($old_pos);\r
+    \r
+                    if (isset($result[2][0]) && $result[2][0] == PDF_TYPE_STREAM) {\r
+                        $result[0] = PDF_TYPE_STREAM;\r
+                    }\r
+    \r
+                               return $result;\r
+                       }\r
+               } else {\r
+                       return $obj_spec;\r
+               }\r
+        }\r
+    \r
+        \r
+        \r
+        /**\r
+         * Reads a token from the file\r
+         *\r
+         * @param object $c pdf_context\r
+         * @return mixed\r
+         */\r
+        function pdf_read_token(&$c)\r
+        {\r
+               // If there is a token available\r
+               // on the stack, pop it out and\r
+               // return it.\r
+    \r
+               if (count($c->stack)) {\r
+                       return array_pop($c->stack);\r
+               }\r
+    \r
+               // Strip away any whitespace\r
+    \r
+               do {\r
+                       if (!$c->ensure_content()) {\r
+                               return false;\r
+                       }\r
+                       $c->offset += strspn($c->buffer, "\x20\x0A\x0C\x0D\x09\x00", $c->offset);\r
+               } while ($c->offset >= $c->length - 1);\r
+    \r
+               // Get the first character in the stream\r
+    \r
+               $char = $c->buffer[$c->offset++];\r
+    \r
+               switch ($char) {\r
+    \r
+                       case '[':\r
+                       case ']':\r
+                       case '(':\r
+                       case ')':\r
+                       \r
+                               // This is either an array or literal string\r
+                               // delimiter, Return it\r
+    \r
+                               return $char;\r
+    \r
+                       case '<':\r
+                       case '>':\r
+    \r
+                               // This could either be a hex string or\r
+                               // dictionary delimiter. Determine the\r
+                               // appropriate case and return the token\r
+    \r
+                               if ($c->buffer[$c->offset] == $char) {\r
+                                       if (!$c->ensure_content()) {\r
+                                           return false;\r
+                                       }\r
+                                       $c->offset++;\r
+                                       return $char . $char;\r
+                               } else {\r
+                                       return $char;\r
+                               }\r
+    \r
+                       case '%':\r
+                           \r
+                           // This is a comment - jump over it!\r
+                           \r
+                    $pos = $c->offset;\r
+                               while(1) {\r
+                                   $match = preg_match("/(\r\n|\r|\n)/", $c->buffer, $m, PREG_OFFSET_CAPTURE, $pos);\r
+                        if ($match === 0) {\r
+                                               if (!$c->increase_length()) {\r
+                                                       return false;\r
+                                               } else {\r
+                               continue;\r
+                               }\r
+                                       }\r
+    \r
+                                       $c->offset = $m[0][1]+strlen($m[0][0]);\r
+                                       \r
+                                       return $this->pdf_read_token($c);\r
+                    }\r
+                    \r
+                       default:\r
+    \r
+                               // This is "another" type of token (probably\r
+                               // a dictionary entry or a numeric value)\r
+                               // Find the end and return it.\r
+    \r
+                               if (!$c->ensure_content()) {\r
+                                       return false;\r
+                               }\r
+    \r
+                               while(1) {\r
+    \r
+                                       // Determine the length of the token\r
+    \r
+                                       $pos = strcspn($c->buffer, "\x20%[]<>()/\x0A\x0C\x0D\x09\x00", $c->offset);\r
+                                       \r
+                                       if ($c->offset + $pos <= $c->length - 1) {\r
+                                               break;\r
+                                       } else {\r
+                                               // If the script reaches this point,\r
+                                               // the token may span beyond the end\r
+                                               // of the current buffer. Therefore,\r
+                                               // we increase the size of the buffer\r
+                                               // and try again--just to be safe.\r
+    \r
+                                               $c->increase_length();\r
+                                       }\r
+                               }\r
+    \r
+                               $result = substr($c->buffer, $c->offset - 1, $pos + 1);\r
+    \r
+                               $c->offset += $pos;\r
+                               return $result;\r
+               }\r
+        }\r
+    }\r
+}\r
diff --git a/mod/assign/feedback/editpdf/fpdi/readme_moodle.txt b/mod/assign/feedback/editpdf/fpdi/readme_moodle.txt
new file mode 100644 (file)
index 0000000..93a9dee
--- /dev/null
@@ -0,0 +1,17 @@
+FPDI
+==================================
+
+No changes from the upstream version have been made. Both FPDI and FPDF_TPL have
+been downloaded and unzipped to this directory.
+
+Information
+-----------
+
+URL: http://www.setasign.de/products/pdf-php-solutions/fpdi/
+Download from: http://www.setasign.de/products/pdf-php-solutions/fpdi/downloads
+Documentation: http://www.setasign.de/products/pdf-php-solutions/fpdi/manuals/
+License: Apache Software License 2.0
+
+Downloaded versions:
+FPDI: 1.4.4
+FPDF_TPL: 1.2.3
diff --git a/mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php b/mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
new file mode 100644 (file)
index 0000000..1b23a21
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'assignfeedback_editpdf', language 'en'
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['addtoquicklist'] = 'Add to quicklist';
+$string['annotationcolour'] = 'Annotation color';
+$string['black'] = 'Black';
+$string['blue'] = 'Blue';
+$string['cannotopenpdf'] = 'Cannot open the pdf file. The file may be corrupt, or in an unsupported format.';
+$string['clear'] = 'Clear';
+$string['colourpicker'] = 'Colour Picker';
+$string['commentcolour'] = 'Comment color';
+$string['comment'] = 'Comments';
+$string['commentcontextmenu'] = 'Comment context menu';
+$string['couldnotsavepage'] = 'Could not save page {$a}';
+$string['currentstamp'] = 'Stamp';
+$string['deleteannotation'] = 'Delete annotation';
+$string['deletecomment'] = 'Delete comment';
+$string['deletefeedback'] = 'Delete feedback PDF';
+$string['downloadablefilename'] = 'feedback.pdf';
+$string['downloadfeedback'] = 'Download feedback PDF';
+$string['editpdf'] = 'Annotate PDF';
+$string['editpdf_help'] = 'Annotate students submissions directly in the browser and produce an edited downloadable PDF.';
+$string['enabled'] = 'Annotate PDF';
+$string['enabled_help'] = 'If enabled, the teacher will be able to create annotated pdf files when marking the assignments. This allows the teacher to add comments, drawing and stamps directly on top of the students work. The annotating is done in the browser and no extra software is required.';
+$string['filter'] = 'Filter comments...';
+$string['generatefeedback'] = 'Generate feedback PDF';
+$string['gotopage'] = 'Go to page';
+$string['green'] = 'Green';
+$string['gspath'] = 'Ghostscript path';
+$string['gspath_help'] = 'On most Linux installs, this can be left as \'/usr/bin/gs\'. On Windows it will be something like \'c:\\gs\\bin\\gswin32c.exe\' (make sure there are no spaces in the path - if necessary copy the files \'gswin32c.exe\' and \'gsdll32.dll\' to a new folder without a space in the path)';
+$string['highlight'] = 'Highlight';
+$string['jsrequired'] = 'Annotating PDF documents requires javascript. Please enable javascript in your browser if you want to use this feature.';
+$string['launcheditor'] = 'Launch PDF editor...';
+$string['line'] = 'Line';
+$string['loadingeditor'] = 'Loading PDF editor';
+$string['navigatenext'] = 'Next page';
+$string['navigateprevious'] = 'Previous page';
+$string['oval'] = 'Oval';
+$string['pagexofy'] = 'Page {$a->page} of {$a->total}';
+$string['pen'] = 'Pen';
+$string['pluginname'] = 'Annotate PDF';
+$string['rectangle'] = 'Rectangle';
+$string['red'] = 'Red';
+$string['searchcomments'] = 'Search comments';
+$string['select'] = 'Select';
+$string['stamppicker'] = 'Stamp picker';
+$string['stampsdesc'] = 'Stamps must be image files. These images can be used with the stamp tool to annotate the PDF.';
+$string['stamps'] = 'Stamps';
+$string['stamp'] = 'Stamp';
+$string['test_doesnotexist'] = 'The ghostscript path points to a non-existent file';
+$string['test_empty'] = 'The ghostscript path is empty - please enter the correct path';
+$string['testgs'] = 'Test ghostscript path';
+$string['test_isdir'] = 'The ghostscript path points to a folder, please include the ghostscript program in the path you specify';
+$string['test_notestfile'] = 'The test PDF is missing';
+$string['test_notexecutable'] = 'The ghostscript points to a file that is not executable';
+$string['test_ok'] = 'The ghostscript path appears to be OK - please check you can see the message in the image below';
+$string['toolbarbutton'] = '{$a->tool} {$a->shortcut}';
+$string['tool'] = 'Tool';
+$string['unsavedchanges'] = 'Unsaved changes';
+$string['viewfeedbackonline'] = 'View annotated pdf...';
+$string['white'] = 'White';
+$string['yellow'] = 'Yellow';
diff --git a/mod/assign/feedback/editpdf/lib.php b/mod/assign/feedback/editpdf/lib.php
new file mode 100644 (file)
index 0000000..80f98e5
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the version information for the comments feedback plugin
+ *
+ * @package assignfeedback_editpdf
+ * @copyright  2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Serves assignment feedback and other files.
+ *
+ * @param mixed $course course or id of the course
+ * @param mixed $cm course module or id of the course module
+ * @param context $context
+ * @param string $filearea
+ * @param array $args
+ * @param bool $forcedownload
+ * @return bool false if file not found, does not return if found - just send the file
+ */
+function assignfeedback_editpdf_pluginfile($course,
+                                           $cm,
+                                           context $context,
+                                           $filearea,
+                                           $args,
+                                           $forcedownload) {
+    global $USER, $DB, $CFG;
+
+    if ($context->contextlevel == CONTEXT_MODULE) {
+
+        require_login($course, false, $cm);
+        $itemid = (int)array_shift($args);
+
+        if (!$assign = $DB->get_record('assign', array('id'=>$cm->instance))) {
+            return false;
+        }
+
+        $record = $DB->get_record('assign_grades', array('id'=>$itemid), 'userid,assignment', MUST_EXIST);
+        $userid = $record->userid;
+        if ($assign->id != $record->assignment) {
+            return false;
+        }
+
+        // Check is users feedback or has grading permission.
+        if ($USER->id != $userid and !has_capability('mod/assign:grade', $context)) {
+            return false;
+        }
+
+        $relativepath = implode('/', $args);
+
+        $fullpath = "/{$context->id}/assignfeedback_editpdf/$filearea/$itemid/$relativepath";
+
+        $fs = get_file_storage();
+        if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
+            return false;
+        }
+        // Download MUST be forced - security!
+        send_stored_file($file, 0, 0, true);// Check if we want to retrieve the stamps.
+    }
+
+}
diff --git a/mod/assign/feedback/editpdf/locallib.php b/mod/assign/feedback/editpdf/locallib.php
new file mode 100644 (file)
index 0000000..9eeb84a
--- /dev/null
@@ -0,0 +1,253 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the definition for the library class for PDF feedback plugin
+ *
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \assignfeedback_editpdf\document_services;
+use \assignfeedback_editpdf\page_editor;
+
+/**
+ * library class for editpdf feedback plugin extending feedback plugin base class
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2012 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assign_feedback_editpdf extends assign_feedback_plugin {
+
+    /**
+     * Get the name of the file feedback plugin
+     * @return string
+     */
+    public function get_name() {
+        return get_string('pluginname', 'assignfeedback_editpdf');
+    }
+
+    /**
+     * Create a widget for rendering the editor.
+     *
+     * @param int $userid
+     * @param stdClass $grade
+     * @param bool $readonly
+     * @return assignfeedback_editpdf_widget
+     */
+    public function get_widget($userid, $grade, $readonly) {
+        $attempt = -1;
+        if ($grade) {
+            $attempt = $grade->attemptnumber;
+        } else {
+            $grade = $this->assignment->get_user_grade($userid, true);
+        }
+
+        $feedbackfile = document_services::get_feedback_document($this->assignment->get_instance()->id,
+                                                                 $userid,
+                                                                 $attempt);
+
+        $stampfiles = array();
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+
+        // Copy any new stamps to this instance.
+        if ($files = $fs->get_area_files($syscontext->id,
+                                         'assignfeedback_editpdf',
+                                         'stamps',
+                                         0,
+                                         "filename",
+                                         false)) {
+            foreach ($files as $file) {
+                $filename = $file->get_filename();
+                if ($filename !== '.') {
+
+                    $existingfile = $fs->get_file($this->assignment->get_context()->id,
+                                                  'assignfeedback_editpdf',
+                                                  'stamps',
+                                                  $grade->id,
+                                                  '/',
+                                                  $file->get_filename());
+                    if (!$existingfile) {
+                        $newrecord = new stdClass();
+                        $newrecord->contextid = $this->assignment->get_context()->id;
+                        $newrecord->itemid = $grade->id;
+                        $fs->create_file_from_storedfile($newrecord, $file);
+                    }
+                }
+            }
+        }
+
+        // Now get the full list of stamp files for this instance.
+        if ($files = $fs->get_area_files($this->assignment->get_context()->id,
+                                         'assignfeedback_editpdf',
+                                         'stamps',
+                                         $grade->id,
+                                         "filename",
+                                         false)) {
+            foreach ($files as $file) {
+                $filename = $file->get_filename();
+                if ($filename !== '.') {
+                    $url = moodle_url::make_pluginfile_url($this->assignment->get_context()->id,
+                                                   'assignfeedback_editpdf',
+                                                   'stamps',
+                                                   $grade->id,
+                                                   '/',
+                                                   $file->get_filename(),
+                                                   false);
+                    array_push($stampfiles, $url->out());
+                }
+            }
+        }
+
+        $url = false;
+        $filename = '';
+        if ($feedbackfile) {
+            $url = moodle_url::make_pluginfile_url($this->assignment->get_context()->id,
+                                                   'assignfeedback_editpdf',
+                                                   document_services::FINAL_PDF_FILEAREA,
+                                                   $grade->id,
+                                                   '/',
+                                                   $feedbackfile->get_filename(),
+                                                   false);
+           $filename = $feedbackfile->get_filename();
+        }
+
+
+        $widget = new assignfeedback_editpdf_widget($this->assignment->get_instance()->id,
+                                                    $userid,
+                                                    $attempt,
+                                                    $url,
+                                                    $filename,
+                                                    $stampfiles,
+                                                    $readonly);
+        return $widget;
+    }
+
+    /**
+     * Get form elements for grading form
+     *
+     * @param stdClass $grade
+     * @param MoodleQuickForm $mform
+     * @param stdClass $data
+     * @param int $userid
+     * @return bool true if elements were added to the form
+     */
+    public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
+        global $PAGE;
+
+        $attempt = -1;
+        if ($grade) {
+            $attempt = $grade->attemptnumber;
+        }
+
+        $files = document_services::list_compatible_submission_files_for_attempt($this->assignment, $userid, $attempt);
+        // Only show the editor if there was a compatible file submitted.
+        if (count($files)) {
+
+            $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
+
+            $widget = $this->get_widget($userid, $grade, false);
+
+            $html = $renderer->render($widget);
+            $mform->addElement('static', 'editpdf', get_string('editpdf', 'assignfeedback_editpdf'), $html);
+            $mform->addHelpButton('editpdf', 'editpdf', 'assignfeedback_editpdf');
+        }
+    }
+
+    /**
+     * Generate the pdf.
+     *
+     * @param stdClass $grade
+     * @param stdClass $data
+     * @return bool
+     */
+    public function save(stdClass $grade, stdClass $data) {
+        if (page_editor::has_annotations_or_comments($grade->id, true)) {
+            document_services::generate_feedback_document($this->assignment, $grade->userid, $grade->attemptnumber);
+        }
+
+        return true;
+    }
+
+    /**
+     * Display the list of files in the feedback status table.
+     *
+     * @param stdClass $grade
+     * @param bool $showviewlink (Always set to false).
+     * @return string
+     */
+    public function view_summary(stdClass $grade, & $showviewlink) {
+        $showviewlink = false;
+        return $this->view($grade);
+    }
+
+    /**
+     * Display the list of files in the feedback status table.
+     *
+     * @param stdClass $grade
+     * @return string
+     */
+    public function view(stdClass $grade) {
+        global $PAGE;
+        $html = '';
+        // Show a link to download the pdf.
+        if (page_editor::has_annotations_or_comments($grade->id, false)) {
+            $html = $this->assignment->render_area_files('assignfeedback_editpdf',
+                                                         document_services::FINAL_PDF_FILEAREA,
+                                                         $grade->id);
+
+            // Also show the link to the read-only interface.
+            $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
+            $widget = $this->get_widget($grade->userid, $grade, true);
+
+            $html .= $renderer->render($widget);
+        }
+        return $html;
+    }
+
+    /**
+     * Return true if there are no released comments/annotations.
+     *
+     * @param stdClass $grade
+     */
+    public function is_empty(stdClass $grade) {
+        global $DB;
+
+        $comments = $DB->count_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$grade->id, 'draft'=>0));
+        $annotations = $DB->count_records('assignfeedback_editpdf_annot', array('gradeid'=>$grade->id, 'draft'=>0));
+        return $comments == 0 && $annotations == 0;
+    }
+
+    /**
+     * The assignment has been deleted - remove the plugin specific data
+     *
+     * @return bool
+     */
+    public function delete_instance() {
+        global $DB;
+        $grades = $DB->get_records('assign_grades', array('assignment'=>$this->assignment->id), '', 'id');
+        list($gradeids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED);
+        $DB->delete_records_select('assignfeedback_editpdf_annot', $gradeids, $params);
+        $DB->delete_records_select('assignfeedback_editpdf_cmnt', $gradeids, $params);
+    }
+
+}
diff --git a/mod/assign/feedback/editpdf/settings.php b/mod/assign/feedback/editpdf/settings.php
new file mode 100644 (file)
index 0000000..d04e762
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Settings for assignfeedback PDF plugin
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2013 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Stamp files setting.
+$name = 'assignfeedback_editpdf/stamps';
+$title = get_string('stamps','assignfeedback_editpdf');
+$description = get_string('stampsdesc', 'assignfeedback_editpdf');
+
+$setting = new admin_setting_configstoredfile($name, $title, $description, 'stamps', 0,
+    array('maxfiles' => 8, 'accepted_types' => array('image')));
+$settings->add($setting);
+
+// Ghostscript setting.
+$settings->add(new admin_setting_configtext('assignfeedback_editpdf/gspath',
+                                            get_string('gspath', 'assignfeedback_editpdf'),
+                                            get_string('gspath_help', 'assignfeedback_editpdf'), '/usr/bin/gs'));
+
+$url = new moodle_url('/mod/assign/feedback/editpdf/testgs.php');
+$link = html_writer::link($url, get_string('testgs', 'assignfeedback_editpdf'));
+$settings->add(new admin_setting_heading('testgs', '', $link));
diff --git a/mod/assign/feedback/editpdf/styles.css b/mod/assign/feedback/editpdf/styles.css
new file mode 100644 (file)
index 0000000..71326ea
--- /dev/null
@@ -0,0 +1,242 @@
+.assignfeedback_editpdf_widget .toolbar ul {
+    display: none;
+}
+
+.assignfeedback_editpdf_widget .toolbar li {
+    list-style-type: none;
+}
+.assignfeedback_editpdf_widget .drawingcanvas {
+    position: relative;
+    min-width: 817px;
+    min-height: 1169px;
+    cursor: crosshair;
+}
+
+.assignfeedback_editpdf_widget {
+    user-select: none;
+    -moz-user-select: none;
+    -webkit-user-select: none;
+    -o-user-select: none;
+}
+
+.assignfeedback_editpdf_widget .pageheader {
+    background-color: #ebebeb;
+    border-bottom: 1px solid #cccccc;
+    padding: 4px;
+    padding-left: 20px;
+    padding-right: 20px;
+    min-height: 50px;
+}
+
+.moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_widget .moodle-dialogue-bd {
+    padding: 0px;
+}
+
+.assignfeedback_editpdf_unsavedchanges.haschanges{
+    display: block;
+}
+.assignfeedback_editpdf_unsavedchanges {
+    display: none;
+    margin-top: 1em;
+}
+.yui3-colourpicker-hidden,
+.yui3-commentsearch-hidden,
+.yui3-commentmenu-hidden {
+    display: none;
+}
+.assignfeedback_editpdf_widget .pageheader button:active {
+    background-color: #ccc;
+}
+.assignfeedback_editpdf_widget .pageheader select,
+.assignfeedback_editpdf_widget .pageheader button {
+    background: none;
+    padding: 4px 10px;
+    border: 0px;
+    border-radius: 0px;
+    margin: 0px;
+    height: 30px;
+    cursor: pointer;
+}
+
+.assignfeedback_editpdf_widget .pageheader .navigation button + button,
+.assignfeedback_editpdf_widget .pageheader .toolbar button + button,
+.assignfeedback_editpdf_widget .pageheader .navigation select + button,
+.assignfeedback_editpdf_widget .pageheader .toolbar select + button {
+    border-left: 1px solid #cccccc;
+    border-right: 0px;
+}
+
+.assignfeedback_editpdf_widget .pageheader .navigation button {
+    border-right: 1px solid #cccccc;
+}
+.assignfeedback_editpdf_widget .pageheader .toolbar,
+.assignfeedback_editpdf_widget .pageheader .navigation-search,
+.assignfeedback_editpdf_widget .pageheader .navigation {
+    border: 1px solid #cccccc;
+    border-bottom-color: #b3b3b3;
+    border-radius: 4px;
+    margin: 10px 4px;
+    background-color: white;
+    height: 30px;
+    line-height: 30px;
+    padding: 0px;
+}
+
+.assignfeedback_editpdf_commentsearch ul {
+    max-height: 400px;
+    overflow-y: auto;
+    padding: 1em;
+}
+.assignfeedback_editpdf_commentsearch ul li {
+    line-height: 1.4em;
+}
+.assignfeedback_editpdf_commentsearch a pre {
+    font-family: helvetica;
+    margin: 0px;
+    padding: 4px;
+}
+
+.assignfeedback_editpdf_widget .navigation-search,
+.assignfeedback_editpdf_widget .navigation {
+    float: left;
+}
+
+.dir-rtl .assignfeedback_editpdf_widget .navigation-search,
+.dir-rtl .assignfeedback_editpdf_widget .navigation {
+    float: right;
+}
+.assignfeedback_editpdf_widget .toolbar {
+    float: right;
+}
+.dir-rtl .assignfeedback_editpdf_widget .toolbar {
+    float: left;
+}
+.assignfeedback_editpdf_widget .navigation,
+.assignfeedback_editpdf_widget .navigation-search,
+.assignfeedback_editpdf_widget .toolbar {
+    display: inline-block;
+}
+
+.assignfeedback_editpdf_colourpicker ul {
+    margin: 0px;
+}
+
+.assignfeedback_editpdf_commentmenu li.quicklist_comment {
+    width: 150px;
+}
+.assignfeedback_editpdf_commentmenu li.quicklist_comment a {
+    white-space: nowrap;
+    display: inline-block;
+    max-width: 130px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+.assignfeedback_editpdf_commentmenu a.delete_quicklist_comment {
+    float: right;
+}
+.dir-rtl .assignfeedback_editpdf_commentmenu a.delete_quicklist_comment {
+    float: left;
+}
+.assignfeedback_editpdf_dropdown button {
+    border: 0px;
+    background: none;
+    padding-left: 12px;
+    padding-right: 12px;
+    padding-top: 6px;
+    padding-bottom: 6px;
+    border-radius: 0px;
+    border-top: 1px solid #cccccc;
+}
+.assignfeedback_editpdf_dropdown li:first-child button {
+    border-top: 0px;
+}
+.moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_dropdown .moodle-dialogue-wrap {
+    box-shadow: none;
+    -moz-box-shadow: none;
+    -webkit-box-shadow: none;
+}
+.moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_dropdown .moodle-dialogue-bd {
+    padding: 0px;
+}
+.assignfeedback_editpdf_dropdown .moodle-dialogue-hd,
+.assignfeedback_editpdf_dropdown .moodle-dialogue-ft {
+    display: none;
+}
+.assignfeedback_editpdf_menu li hr {
+    margin: 0px;
+}
+.assignfeedback_editpdf_menu li a {
+    text-decoration: none;
+    color: #555;
+    margin: 10px;
+}
+.assignfeedback_editpdf_menu li:hover,
+.assignfeedback_editpdf_menu li:hover a,
+.assignfeedback_editpdf_menu li a:hover {
+    background-color: #08c;
+    color: white;
+    text-decoration: underline;
+}
+ul.assignfeedback_editpdf_menu {
+    margin: 0px;
+}
+.assignfeedback_editpdf_menu li {
+    list-style-type: none;
+    margin: 0px;
+    border-radius: 4px;
+}
+.assignfeedback_editpdf_menu li button {
+    margin: 0px;
+}
+
+.assignfeedback_editpdf_widget .pageheader button.assignfeedback_editpdf_selectedbutton:hover,
+.assignfeedback_editpdf_widget .pageheader button.assignfeedback_editpdf_selectedbutton {
+    background-color: #CCF;
+    background-image: none;
+}
+
+.assignfeedback_editpdf_widget .commentdrawable img {
+    padding: 1px;
+}
+
+.assignfeedback_editpdf_widget .commentdrawable a {
+    float: right;
+    position: relative;
+    left: -17px;
+    top: 2px;
+    height: 14px;
+    background-color: white;
+    border-left: 1px solid #ccc;
+    border-bottom: 1px solid #ccc;
+    line-height: 14px;
+}
+
+.dir-rtl .assignfeedback_editpdf_widget .commentdrawable a {
+    float: left;
+    left: none;
+    right: -17px;
+    border-left: 0px;
+    border-right: 1px solid #ccc;
+}
+
+.assignfeedback_editpdf_widget .commentdrawable textarea {
+    padding: 4px;
+    padding-right: 20px;
+    resize: none;
+    overflow: hidden;
+    color: black;
+    border: 2px solid #ccc;
+    border-radius: 4px;
+    font-size: 16px;
+    font-family: helvetica;
+    min-height: 1.2em;
+}
+
+.assignfeedback_editpdf_widget .commentdrawable {
+    display: inline-block;
+}
+
+.dir-rtl .assignfeedback_editpdf_widget .commentdrawable textarea {
+    padding-left: 20px;
+    padding-right: 4px;
+}
diff --git a/mod/assign/feedback/editpdf/testgs.php b/mod/assign/feedback/editpdf/testgs.php
new file mode 100644 (file)
index 0000000..aa1a2c0
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test that ghostscript is configured correctly
+ *
+ * @package   assignfeedback_editpdf
+ * @copyright 2013 Davo Smith
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__).'/../../../../config.php');
+global $PAGE, $OUTPUT;
+
+$PAGE->set_url(new moodle_url('/mod/assign/feedback/editpdf/testgs.php'));
+$PAGE->set_context(context_system::instance());
+
+require_capability('moodle/site:config', context_system::instance());
+
+if (optional_param('sendimage', false, PARAM_BOOL)) {
+    // Serve the generated test image.
+    assignfeedback_editpdf\pdf::send_test_image();
+    die();
+}
+
+$result = assignfeedback_editpdf\pdf::test_gs_path();
+
+switch ($result->status) {
+    case assignfeedback_editpdf\pdf::GSPATH_OK:
+        $msg = get_string('test_ok', 'assignfeedback_editpdf');
+        $msg .= html_writer::empty_tag('br');
+        $imgurl = new moodle_url($PAGE->url, array('sendimage' => 1));
+        $msg .= html_writer::empty_tag('img', array('src' => $imgurl));
+        break;
+
+    case assignfeedback_editpdf\pdf::GSPATH_ERROR:
+        $msg = $result->message;
+        break;
+
+    default:
+        $msg = get_string("test_{$result->status}", 'assignfeedback_editpdf');
+        break;
+}
+
+$returl = new moodle_url('/admin/settings.php', array('section' => 'assignfeedback_editpdf'));
+$msg .= $OUTPUT->continue_button($returl);
+
+$strheading = get_string('testgs', 'assignfeedback_editpdf');
+$PAGE->set_heading($strheading);
+$PAGE->set_title($strheading);
+
+echo $OUTPUT->header();
+echo $OUTPUT->box($msg, 'generalbox ');
+echo $OUTPUT->footer();
diff --git a/mod/assign/feedback/editpdf/tests/editpdf_test.php b/mod/assign/feedback/editpdf/tests/editpdf_test.php
new file mode 100644 (file)
index 0000000..1e64c5e
--- /dev/null
@@ -0,0 +1,302 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for assignfeedback_editpdf\comments_quick_list
+ *
+ * @package    assignfeedback_editpdf
+ * @category   phpunit
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \assignfeedback_editpdf\comments_quick_list;
+use \assignfeedback_editpdf\document_services;
+use \assignfeedback_editpdf\page_editor;
+use \assignfeedback_editpdf\pdf;
+use \assignfeedback_editpdf\comment;
+use \assignfeedback_editpdf\annotation;
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Unit tests for assignfeedback_editpdf\comments_quick_list
+ *
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_editpdf_testcase extends mod_assign_base_testcase {
+
+    protected function setUp() {
+        // Skip this test if ghostscript is not supported.
+        if (!pdf::test_gs_path(false)) {
+            $this->markTestSkipped('Ghostscript not setup');
+            return;
+        }
+        parent::setUp();
+    }
+
+    protected function create_assign_and_submit_pdf() {
+        global $CFG;
+        $assign = $this->create_instance(array('assignsubmission_file_enabled' => 1,
+                                               'assignsubmission_file_maxfiles' => 1,
+                                               'assignfeedback_editpdf_enabled' => 1,
+                                               'assignsubmission_file_maxsizebytes' => 1000000));
+
+        $user = $this->students[0];
+        $this->setUser($user);
+
+        // Create a file submission with the test pdf.
+        $submission = $assign->get_user_submission($user->id, true);
+
+        $fs = get_file_storage();
+        $pdfsubmission = (object) array(
+            'contextid' => $assign->get_context()->id,
+            'component' => 'assignsubmission_file',
+            'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA,
+            'itemid' => $submission->id,
+            'filepath' => '/',
+            'filename' => 'submission.pdf'
+        );
+        $sourcefile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf';
+        $fi = $fs->create_file_from_pathname($pdfsubmission, $sourcefile);
+
+        $data = new stdClass();
+        $plugin = $assign->get_submission_plugin_by_type('file');
+        $plugin->save($submission, $data);
+
+        return $assign;
+    }
+
+    public function test_comments_quick_list() {
+
+        $this->setUser($this->teachers[0]);
+
+        $comments = comments_quick_list::get_comments();
+
+        $this->assertEmpty($comments);
+
+        $comment = comments_quick_list::add_comment('test', 45, 'red');
+
+        $comments = comments_quick_list::get_comments();
+
+        $this->assertEquals(count($comments), 1);
+        $first = reset($comments);
+        $this->assertEquals($comment, $first);
+
+        $commentbyid = comments_quick_list::get_comment($comment->id);
+        $this->assertEquals($comment, $commentbyid);
+
+        $result = comments_quick_list::remove_comment($comment->id);
+
+        $this->assertTrue($result);
+
+        $comments = comments_quick_list::get_comments();
+        $this->assertEmpty($comments);
+    }
+
+    public function test_page_editor() {
+
+        $assign = $this->create_assign_and_submit_pdf();
+        $this->setUser($this->teachers[0]);
+
+        $grade = $assign->get_user_grade($this->students[0]->id, true);
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+        $this->assertFalse($notempty);
+
+        $comment = new comment();
+
+        $comment->rawtext = 'Comment text';
+        $comment->width = 100;
+        $comment->x = 100;
+        $comment->y = 100;
+        $comment->colour = 'red';
+
+        $comment2 = new comment();
+
+        $comment2->rawtext = 'Comment text 2';
+        $comment2->width = 100;
+        $comment2->x = 200;
+        $comment2->y = 100;
+        $comment2->colour = 'clear';
+
+        page_editor::set_comments($grade->id, 0, array($comment, $comment2));
+
+        $annotation = new annotation();
+
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'line';
+        $annotation->colour = 'red';
+
+        $annotation2 = new annotation();
+
+        $annotation2->path = '';
+        $annotation2->x = 100;
+        $annotation2->y = 100;
+        $annotation2->endx = 200;
+        $annotation2->endy = 200;
+        $annotation2->type = 'rectangle';
+        $annotation2->colour = 'yellow';
+
+        page_editor::set_annotations($grade->id, 0, array($annotation, $annotation2));
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+        // Still empty because all edits are still drafts.
+        $this->assertFalse($notempty);
+
+        $comments = page_editor::get_comments($grade->id, 0, false);
+
+        $this->assertEmpty($comments);
+
+        $comments = page_editor::get_comments($grade->id, 0, true);
+
+        $this->assertEquals(count($comments), 2);
+
+        $annotations = page_editor::get_annotations($grade->id, 0, false);
+
+        $this->assertEmpty($annotations);
+
+        $annotations = page_editor::get_annotations($grade->id, 0, true);
+
+        $this->assertEquals(count($annotations), 2);
+
+        $comment = reset($comments);
+        $annotation = reset($annotations);
+
+        page_editor::remove_comment($comment->id);
+        page_editor::remove_annotation($annotation->id);
+
+        $comments = page_editor::get_comments($grade->id, 0, true);
+
+        $this->assertEquals(count($comments), 1);
+
+        $annotations = page_editor::get_annotations($grade->id, 0, true);
+
+        $this->assertEquals(count($annotations), 1);
+
+        page_editor::release_drafts($grade->id);
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+
+        $this->assertTrue($notempty);
+
+        page_editor::unrelease_drafts($grade->id);
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+
+        $this->assertFalse($notempty);
+    }
+
+    public function test_document_services() {
+
+        $assign = $this->create_assign_and_submit_pdf();
+        $this->setUser($this->teachers[0]);
+
+        $grade = $assign->get_user_grade($this->students[0]->id, true);
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+        $this->assertFalse($notempty);
+
+        $comment = new comment();
+
+        $comment->rawtext = 'Comment text';
+        $comment->width = 100;
+        $comment->x = 100;
+        $comment->y = 100;
+        $comment->colour = 'red';
+
+        page_editor::set_comments($grade->id, 0, array($comment));
+
+        $annotations = array();
+
+        $annotation = new annotation();
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'line';
+        $annotation->colour = 'red';
+        array_push($annotations, $annotation);
+
+        $annotation = new annotation();
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'rectangle';
+        $annotation->colour = 'yellow';
+        array_push($annotations, $annotation);
+
+        $annotation = new annotation();
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'oval';
+        $annotation->colour = 'green';
+        array_push($annotations, $annotation);
+
+        $annotation = new annotation();
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 116;
+        $annotation->type = 'highlight';
+        $annotation->colour = 'blue';
+        array_push($annotations, $annotation);
+
+        $annotation = new annotation();
+        $annotation->path = '100,100:105,105:110,100';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 110;
+        $annotation->endy = 105;
+        $annotation->type = 'pen';
+        $annotation->colour = 'black';
+        array_push($annotations, $annotation);
+        page_editor::set_annotations($grade->id, 0, $annotations);
+
+        page_editor::release_drafts($grade->id);
+
+        $notempty = page_editor::has_annotations_or_comments($grade->id, false);
+
+        $this->assertTrue($notempty);
+
+        $file = document_services::generate_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
+        $this->assertNotEmpty($file);
+
+        $file2 = document_services::get_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
+
+        $this->assertEquals($file, $file2);
+
+        document_services::delete_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
+        $file3 = document_services::get_feedback_document($assign->get_instance()->id, $grade->userid, $grade->attemptnumber);
+
+        $this->assertEmpty($file3);
+    }
+}
diff --git a/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf b/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf
new file mode 100644 (file)
index 0000000..576d378
Binary files /dev/null and b/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf differ
diff --git a/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf b/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf
new file mode 100644 (file)
index 0000000..f96796a
Binary files /dev/null and b/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf differ
diff --git a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
new file mode 100644 (file)
index 0000000..2897ba0
Binary files /dev/null and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
diff --git a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
new file mode 100644 (file)
index 0000000..13f0e94
Binary files /dev/null and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
diff --git a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
new file mode 100644 (file)
index 0000000..2897ba0
Binary files /dev/null and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
diff --git a/mod/assign/feedback/editpdf/yui/src/editor/build.json b/mod/assign/feedback/editpdf/yui/src/editor/build.json
new file mode 100644 (file)
index 0000000..d61260b
--- /dev/null
@@ -0,0 +1,30 @@
+{
+  "name": "moodle-assignfeedback_editpdf-editor",
+  "builds": {
+    "moodle-assignfeedback_editpdf-editor": {
+      "jsfiles": [
+        "globals.js",
+        "point.js",
+        "rect.js",
+        "edit.js",
+        "drawable.js",
+        "annotation.js",
+        "annotationline.js",
+        "annotationrectangle.js",
+        "annotationoval.js",
+        "annotationpen.js",
+        "annotationhighlight.js",
+        "annotationstamp.js",
+        "dropdown.js",
+        "colourpicker.js",
+        "stamppicker.js",
+        "commentmenu.js",
+        "commentsearch.js",
+        "comment.js",
+        "quickcomment.js",
+        "quickcommentlist.js",
+        "editor.js"
+      ]
+    }
+  }
+}
diff --git a/mod/assign/feedback/editpdf/yui/src/editor/js/annotation.js b/mod/assign/feedback/editpdf/yui/src/editor/js/annotation.js
new file mode 100644 (file)
index 0000000..ac1c848
--- /dev/null
@@ -0,0 +1,327 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class representing a list of annotations.
+ *
+ * @module moodle-assignfeedback_editpdf-editor
+ */
+ANNOTATION = function(config) {
+    ANNOTATION.superclass.constructor.apply(this, [config]);
+};
+
+ANNOTATION.NAME = "annotation";
+ANNOTATION.ATTRS = {};
+
+Y.extend(ANNOTATION, Y.Base, {
+    /**
+     * Reference to M.assignfeedback_editpdf.editor.
+     * @property editor
+     * @type M.assignfeedback_editpdf.editor
+     * @public
+     */
+    editor : null,
+
+    /**
+     * Grade id
+     * @property gradeid
+     * @type Int
+     * @public
+     */
+    gradeid : 0,
+
+    /**
+     * Comment page number
+     * @property pageno
+     * @type Int
+     * @public
+     */
+    pageno : 0,
+
+    /**
+     * X position
+     * @property x
+     * @type Int
+     * @public
+     */
+    x : 0,
+
+    /**
+     * Y position
+     * @property y
+     * @type Int
+     * @public
+     */
+    y : 0,
+
+    /**
+     * Ending x position
+     * @property endx
+     * @type Int
+     * @public
+     */
+    endx : 0,
+
+    /**
+     * Ending y position
+     * @property endy
+     * @type Int
+     * @public
+     */
+    endy : 0,
+
+    /**
+     * Path
+     * @property path
+     * @type String - list of points like x1,y1:x2,y2
+     * @public
+     */
+    path : '',
+
+    /**
+     * Tool.
+     * @property type
+     * @type String
+     * @public
+     */
+    type : 'rect',
+
+    /**
+     * Annotation colour.
+     * @property colour
+     * @type String
+     * @public
+     */
+    colour : 'red',
+
+    /**
+     * Reference to M.assignfeedback_editpdf.drawable
+     * @property drawable
+     * @type M.assignfeedback_editpdf.drawable
+     * @public
+     */
+    drawable : false,
+
+    /**
+     * Initialise the annotation.
+     *
+     * @method initializer
+     * @return void
+     */
+    initializer : function(config) {
+        this.editor = config.editor || null;
+        this.gradeid = parseInt(config.gradeid, 10) || 0;
+        this.pageno = parseInt(config.pageno, 10) || 0;
+        this.x = parseInt(config.x, 10) || 0;
+        this.y = parseInt(config.y, 10) || 0;
+        this.endx = parseInt(config.endx, 10) || 0;
+        this.endy = parseInt(config.endy, 10) || 0;
+        this.path = config.path || '';
+        this.type = config.type || 'rect';
+        this.colour = config.colour || 'red';
+        this.drawable = false;
+    },
+
+    /**
+     * Clean a comment record, returning an oject with only fields that are valid.
+     * @public
+     * @method clean
+     * @return {}
+     */
+    clean : function() {
+        return {
+            gradeid : this.gradeid,
+            x : parseInt(this.x, 10),
+            y : parseInt(this.y, 10),
+            endx : parseInt(this.endx, 10),
+            endy : parseInt(this.endy, 10),
+            type : this.type,
+            path : this.path,
+            pageno : this.pageno,
+            colour : this.colour
+        };
+    },
+
+    /**
+     * Draw a selection around this annotation if it is selected.
+     * @public
+     * @method draw_highlight
+     * @return M.assignfeedback_editpdf.drawable
+     */
+    draw_highlight : function() {
+        var bounds,
+            drawingregion = Y.one(SELECTOR.DRAWINGREGION),
+            offsetcanvas = Y.one(SELECTOR.DRAWINGCANVAS).getXY(),
+            shape;
+
+        if (this.editor.currentannotation === this) {
+            // Draw a highlight around the annotation.
+            bounds = new M.assignfeedback_editpdf.rect();
+            bounds.bound([new M.assignfeedback_editpdf.point(this.x, this.y),
+                          new M.assignfeedback_editpdf.point(this.endx, this.endy)]);
+
+            shape = this.editor.graphic.addShape({
+                type: Y.Rect,
+                width: bounds.width,
+                height: bounds.height,
+                stroke: {
+                   weight: STROKEWEIGHT,
+                   color: SELECTEDBORDERCOLOUR
+                },
+                fill: {
+                   color: SELECTEDFILLCOLOUR
+                },
+                x: bounds.x,
+                y: bounds.y
+            });
+            this.drawable.shapes.push(shape);
+
+            // Add a delete X to the annotation.
+            var deleteicon = Y.Node.create('<img src="' + M.util.image_url('trash', 'assignfeedback_editpdf') + '"/>'),
+                deletelink = Y.Node.create('<a href="#" role="button"></a>');
+
+            deleteicon.setAttrs({
+                'alt': M.util.get_string('deleteannotation', 'assignfeedback_editpdf')
+            });
+            deleteicon.setStyles({
+                'backgroundColor' : 'white'
+            });
+            deletelink.addClass('deleteannotationbutton');
+            deletelink.append(deleteicon);
+
+            drawingregion.append(deletelink);
+            deletelink.setData('annotation', this);
+            deletelink.setStyle('zIndex', '200');
+
+            deletelink.on('click', this.remove, this);
+            deletelink.on('key', this.remove, 'space,enter', this);
+
+            deletelink.setX(offsetcanvas[0] + bounds.x + bounds.width - 18);
+            deletelink.setY(offsetcanvas[1] + bounds.y + 6);
+            this.drawable.nodes.push(deletelink);
+        }
+        return this.drawable;
+    },
+
+    /**
+     * Draw an annotation
+     * @public
+     * @method draw
+     * @return M.assignfeedback_editpdf.drawable|false
+     */
+    draw : function() {
+        // Should be overridden by the subclass.
+        this.draw_highlight();
+        return this.drawable;
+    },
+
+    /**
+     * Delete an annotation
+     * @protected
+     * @method remove
+     * @param event
+     */
+    remove : function() {
+        var annotations;
+
+        annotations = this.editor.pages[this.editor.currentpage].annotations;
+        for (i = 0; i < annotations.length; i++) {
+            if (annotations[i] === this) {
+                annotations.splice(i, 1);
+                if (this.drawable) {
+                    this.drawable.erase();
+                }
+                this.editor.currentannotation = false;
+                this.editor.save_current_page();
+                return;
+            }
+        }
+    },
+
+    /**
+     * Move an annotation to a new location.
+     * @public
+     * @param int newx
+     * @param int newy
+     * @method move_annotation
+     */
+    move : function(newx, newy) {
+        var diffx = newx - this.x,
+            diffy = newy - this.y,
+            newpath, oldpath, xy,
+            x, y;
+
+        this.x += diffx;
+        this.y += diffy;
+        this.endx += diffx;
+        this.endy += diffy;
+
+        if (this.path) {
+            newpath = [];
+            oldpath = this.path.split(':');
+            Y.each(oldpath, function(position) {
+                xy = position.split(',');
+                x = parseInt(xy[0], 10);
+                y = parseInt(xy[1], 10);
+                newpath.push((x + diffx) + ',' + (y + diffy));
+            });
+
+            this.path = newpath.join(':');
+
+        }
+        if (this.drawable) {
+            this.drawable.erase();
+        }
+        this.editor.drawables.push(this.draw());
+    },
+
+    /**
+     * Draw the in progress edit.
+     *
+     * @public
+     * @method draw_current_edit
+     * @param M.assignfeedback_editpdf.edit edit
+     */
+    draw_current_edit : function(edit) {
+        var noop = edit && false;
+        // Override me please.
+        return noop;
+    },
+
+    /**
+     * Promote the current edit to a real annotation.
+     *
+     * @public
+     * @method init_from_edit
+     * @param M.assignfeedback_editpdf.edit edit
+     */
+    init_from_edit : function(edit) {
+        var bounds = new M.assignfeedback_editpdf.rect();
+        bounds.bound([edit.start, edit.end]);
+
+        this.gradeid = this.editor.get('gradeid');
+        this.pageno = this.editor.currentpage;
+        this.x = bounds.x;
+        this.y = bounds.y;
+        this.endx = bounds.x + bounds.width;
+        this.endy = bounds.y + bounds.height;
+        this.colour = edit.annotationcolour;
+        this.path = '';
+    }
+
+});
+
+M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
+M.assignfeedback_editpdf.annotation = ANNOTATION;
diff --git a/mod/assign/feedback/editpdf/yui/src/editor/js/annotationhighlight.js b/mod/assign/feedback/editpdf/yui/src/editor/js/annotationhighlight.js
new file mode 100644 (file)
index 0000000..46c8960
--- /dev/null
@@ -0,0 +1,139 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class representing a highlight.
+ *
+ * @namespace M.assignfeedback_editpdf
+ * @class annotationhighlight
+ * @extends annotation
+ * @module moodle-assignfeedback_editpdf-editor
+ */
+ANNOTATIONHIGHLIGHT = function(config) {
+    ANNOTATIONHIGHLIGHT.superclass.constructor.apply(this, [config]);
+};
+
+ANNOTATIONHIGHLIGHT.NAME = "annotationhighlight";
+ANNOTATIONHIGHLIGHT.ATTRS = {};
+
+Y.extend(ANNOTATIONHIGHLIGHT, M.assignfeedback_editpdf.annotation, {
+    /**
+     * Draw a highlight annotation
+     * @protected
+     * @method draw
+     * @return M.assignfeedback_editpdf.drawable
+     */
+    draw : function() {
+        var drawable,
+            shape,
+            bounds,
+            highlightcolour;
+
+        drawable = new M.assignfeedback_editpdf.drawable(this.editor);
+        bounds = new M.assignfeedback_editpdf.rect();
+        bounds.bound([new M.assignfeedback_editpdf.point(this.x, this.y),
+                      new M.assignfeedback_editpdf.point(this.endx, this.endy)]);
+
+        highlightcolour = ANNOTATIONCOLOUR[this.colour];
+
+        // Add an alpha channel to the rgb colour.
+
+        highlightcolour = highlightcolour.replace('rgb', 'rgba');
+        highlightcolour = highlightcolour.replace(')', ',0.5)');
+
+        shape = this.editor.graphic.addShape({
+            type: Y.Rect,
+            width: bounds.width,
+            height: bounds.height,
+            stroke: false,
+            fill: {
+                color: highlightcolour
+            },
+            x: bounds.x,
+            y: bounds.y
+        });
+
+        drawable.shapes.push(shape);
+        this.drawable = drawable;
+
+        return ANNOTATIONHIGHLIGHT.superclass.draw.apply(this);
+    },
+
+    /**
+     * Draw the in progress edit.
+     *
+     * @public
+     * @method draw_current_edit
+     * @param M.assignfeedback_editpdf.edit edit
+     */
+    draw_current_edit : function(edit) {
+        var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
+            shape,
+            bounds,
+            highlightcolour;
+
+        bounds = new M.assignfeedback_editpdf.rect();
+        bounds.bound([new M.assignfeedback_editpdf.point(edit.start.x, edit.start.y),
+                      new M.assignfeedback_editpdf.point(edit.end.x, edit.end.y)]);
+
+        highlightcolour = ANNOTATIONCOLOUR[edit.annotationcolour];
+        // Add an alpha channel to the rgb colour.
+
+        highlightcolour = highlightcolour.replace('rgb', 'rgba');
+        highlightcolour = highlightcolour.replace(')', ',0.5)');
+
+        // We will draw a box with the current background colour.
+        shape = this.editor.graphic.addShape({
+            type: Y.Rect,
+            width: bounds.width,
+            height: 16,
+            stroke: false,
+            fill: {
+               color: highlightcolour
+            },
+            x: bounds.x,
+            y: edit.start.y
+        });
+
+        drawable.shapes.push(shape);
+
+        return drawable;
+    },
+
+    /**
+     * Promote the current edit to a real annotation.
+     *
+     * @public
+     * @method init_from_edit
+     * @param M.assignfeedback_editpdf.edit edit
+     */
+    init_from_edit : function(edit) {
+        var bounds = new M.assignfeedback_editpdf.rect();
+        bounds.bound([edit.start, edit.end]);
+
+        this.gradeid = this.editor.get('gradeid');
+        this.pageno = this.editor.currentpage;
+        this.x = bounds.x;
+        this.y = edit.start.y;
+        this.endx = bounds.x + bounds.width;
+        this.endy = edit.start.y + 16;
+        this.colour = edit.annotationcolour;
+        this.page = '';
+    }
+
+});
+
+M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
+M.assignfeedback_editpdf.annotationhighlight = ANNOTATIONHIGHLIGHT;
index 5b816f5..b7366bf 100644 (file)
@@ -45,10 +45,10 @@ Y.extend(ANNOTATIONSTAMP, M.assignfeedback_editpdf.annotation, {
         node = Y.Node.create('<div/>');
         node.setStyles({
             'display': 'inline-block',
-            'backgroundImage': 'url(' + this.path + ')',
+            'backgroundImage': 'url(' + this.editor.get_stamp_image_url(this.path) + ')',
             'width': (this.endx - this.x),
             'height': (this.endy - this.y),
-            'backgroundSize': '100%',
+            'backgroundSize': '100% 100%',
             'zIndex': 50
         });
 
@@ -87,10 +87,10 @@ Y.extend(ANNOTATIONSTAMP, M.assignfeedback_editpdf.annotation, {
         node = Y.Node.create('<div/>');
         node.setStyles({
             'display': 'inline-block',
-            'backgroundImage': 'url(' + edit.stamp + ')',
+            'backgroundImage': 'url(' + this.editor.get_stamp_image_url(edit.stamp) + ')',
             'width': bounds.width,
             'height': bounds.height,
-            'backgroundSize': '100%',
+            'backgroundSize': '100% 100%',
             'zIndex': 50
         });
 
diff --git a/mod/assign/feedback/editpdf/yui/src/editor/js/colourpicker.js b/mod/assign/feedback/editpdf/yui/src/editor/js/colourpicker.js
new file mode 100644 (file)
index 0000000..196eb8a
--- /dev/null
@@ -0,0 +1,122 @@
+var COLOURPICKER_NAME = "Colourpicker",
+    COLOURPICKER;
+
+/**
+ * COLOURPICKER
+ * This is a drop down list of colours.
+ *
+ * @namespace M.assignfeedback_editpdf.colourpicker
+ * @class dropdown
+ * @constructor
+ * @extends Y.Base
+ */
+COLOURPICKER = function(config) {
+    COLOURPICKER.superclass.constructor.apply(this, [config]);
+};
+
+Y.extend(COLOURPICKER, M.assignfeedback_editpdf.dropdown, {
+
+    /**
+     * Initialise the menu.
+     *
+     * @method initializer
+     * @return void
+     */
+    initializer : function(config) {
+        var colourlist = Y.Node.create('<ul role="menu" class="assignfeedback_editpdf_menu"/>'),
+            body;
+
+        // Build a list of coloured buttons.
+        Y.each(this.get('colours'), function(rgb, colour) {
+            var button, listitem, title, img, iconname;
+
+            title = M.util.get_string(colour, 'assignfeedback_editpdf');
+            iconname = this.get('iconprefix') + colour;
+            img = M.util.image_url(iconname, 'assignfeedback_editpdf');
+            button = Y.Node.create('<button><img alt="' + title + '" src="' + img + '"/></button>');
+            button.setAttribute('data-colour', colour);
+            button.setAttribute('data-rgb', rgb);
+            button.setStyle('backgroundImage', 'none');
+            listitem = Y.Node.create('<li/>');
+            listitem.append(button);
+            colourlist.append(listitem);
+        }, this);
+
+        body = Y.Node.create('<div/>');
+
+        // Set the call back.
+        colourlist.delegate('click', this.callback_handler, 'button', this);
+        colourlist.delegate('key', this.callback_handler, 'down:13', 'button', this);
+
+        // Set the accessible header text.
+        this.set('headerText', M.util.get_string('colourpicker', 'assignfeedback_editpdf'));
+
+        // Set the body content.
+        body.append(colourlist);
+        this.set('bodyContent', body);
+
+        COLOURPICKER.superclass.initializer.call(this, config);
+    },
+    callback_handler : function(e) {
+        var callback = this.get('callback'),
+            callbackcontext = this.get('context'),
+            bind;
+
+        this.hide();
+
+        // Call the callback with the specified context.
+        bind = Y.bind(callback, callbackcontext, e);
+
+        bind();
+    }
+}, {
+    NAME : COLOURPICKER_NAME,
+    ATTRS : {
+        /**
+         * The list of colours this colour picker supports.
+         *
+