first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,87 @@
<?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';
/** @var int draft status, default 1 = true */
public $draft = 1;
/**
* Convert a compatible stdClass into an instance of this class.
* @param \stdClass $record
*/
public function __construct(\stdClass $record = null) {
if ($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;
}
}
}
}
}
}
@@ -0,0 +1,446 @@
<?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 combined document class for the assignfeedback_editpdf plugin.
*
* @package assignfeedback_editpdf
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignfeedback_editpdf;
defined('MOODLE_INTERNAL') || die();
/**
* The combined_document class for the assignfeedback_editpdf plugin.
*
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class combined_document {
/**
* Status value representing a conversion waiting to start.
*/
const STATUS_PENDING_INPUT = 0;
/**
* Status value representing all documents ready to be combined.
*/
const STATUS_READY = 1;
/**
* Status value representing all documents are ready to be combined as are supported.
*/
const STATUS_READY_PARTIAL = 3;
/**
* Status value representing a successful conversion.
*/
const STATUS_COMPLETE = 2;
/**
* Status value representing a permanent error.
*/
const STATUS_FAILED = -1;
/**
* The list of files which make this document.
*/
protected $sourcefiles = [];
/**
* The resultant combined file.
*/
protected $combinedfile;
/**
* The combination status.
*/
protected $combinationstatus = null;
/**
* The number of pages in the combined PDF.
*/
protected $pagecount = 0;
/**
* Check the current status of the document combination.
* Note that the combined document may not contain all the source files if some of the
* source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only
* the cover sheet will be included in the combined document.
*
* @return int
*/
public function get_status() {
if ($this->combinedfile) {
// The combined file exists. Report success.
return self::STATUS_COMPLETE;
}
if (empty($this->sourcefiles)) {
// There are no source files to combine.
return self::STATUS_FAILED;
}
if (!empty($this->combinationstatus)) {
// The combination is in progress and has set a status.
// Return it instead.
return $this->combinationstatus;
}
$pending = false;
$partial = false;
foreach ($this->sourcefiles as $file) {
// The combined file has not yet been generated.
// Check the status of each source file.
if (is_a($file, \core_files\conversion::class)) {
$status = $file->get('status');
switch ($status) {
case \core_files\conversion::STATUS_IN_PROGRESS:
case \core_files\conversion::STATUS_PENDING:
$pending = true;
break;
// There are 4 status flags, so the only remaining one is complete which is fine.
case \core_files\conversion::STATUS_FAILED:
$partial = true;
break;
}
}
}
if ($pending) {
return self::STATUS_PENDING_INPUT;
} else {
if ($partial) {
return self::STATUS_READY_PARTIAL;
}
return self::STATUS_READY;
}
}
/**
* Set the completed combined file.
*
* @param \stored_file $file The completed document for all files to be combined.
* @return $this
*/
public function set_combined_file($file) {
$this->combinedfile = $file;
return $this;
}
/**
* Return true of the combined file contained only some of the submission files.
*
* @return boolean
*/
public function is_partial_conversion() {
$combinedfile = $this->get_combined_file();
if (empty($combinedfile)) {
return false;
}
$filearea = $combinedfile->get_filearea();
return $filearea == document_services::PARTIAL_PDF_FILEAREA;
}
/**
* Retrieve the completed combined file.
*
* @return stored_file
*/
public function get_combined_file() {
return $this->combinedfile;
}
/**
* Set all source files which are to be combined.
*
* @param \stored_file|conversion[] $files The complete list of all source files to be combined.
* @return $this
*/
public function set_source_files($files) {
$this->sourcefiles = $files;
return $this;
}
/**
* Add an additional source file to the end of the existing list.
*
* @param \stored_file|conversion $file The file to add to the end of the list.
* @return $this
*/
public function add_source_file($file) {
$this->sourcefiles[] = $file;
return $this;
}
/**
* Retrieve the complete list of source files.
*
* @return stored_file|conversion[]
*/
public function get_source_files() {
return $this->sourcefiles;
}
/**
* Refresh the files.
*
* This includes polling any pending conversions to see if they are complete.
*
* @return $this
*/
public function refresh_files() {
$converter = new \core_files\converter();
foreach ($this->sourcefiles as $file) {
if (is_a($file, \core_files\conversion::class)) {
$status = $file->get('status');
switch ($status) {
case \core_files\conversion::STATUS_COMPLETE:
continue 2;
break;
default:
$converter->poll_conversion($conversion);
}
}
}
return $this;
}
/**
* Combine all source files into a single PDF and store it in the
* file_storage API using the supplied contextid and itemid.
*
* @param int $contextid The contextid for the file to be stored under
* @param int $itemid The itemid for the file to be stored under
* @return $this
*/
public function combine_files($contextid, $itemid) {
global $CFG;
$currentstatus = $this->get_status();
$readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL];
if ($currentstatus === self::STATUS_FAILED) {
$this->store_empty_document($contextid, $itemid);
return $this;
} else if (!in_array($currentstatus, $readystatuslist)) {
// The document is either:
// * already combined; or
// * pending input being fully converted; or
// * unable to continue due to an issue with the input documents.
//
// Exit early as we cannot continue.
return $this;
}
require_once($CFG->libdir . '/pdflib.php');
$pdf = new pdf();
$files = $this->get_source_files();
$compatiblepdfs = [];
foreach ($files as $file) {
// Check that each file is compatible and add it to the list.
// Note: We drop non-compatible files.
$compatiblepdf = false;
if (is_a($file, \core_files\conversion::class)) {
$status = $file->get('status');
if ($status == \core_files\conversion::STATUS_COMPLETE) {
$compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
}
} else {
$compatiblepdf = pdf::ensure_pdf_compatible($file);
}
if ($compatiblepdf) {
$compatiblepdfs[] = $compatiblepdf;
}
}
$tmpdir = make_request_directory();
$tmpfile = $tmpdir . '/' . document_services::COMBINED_PDF_FILENAME;
try {
$pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
$pdf->Close();
} catch (\Exception $e) {
// Unable to combine the PDF.
debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);
$pdf->Close();
return $this->mark_combination_failed();
}
// Verify the PDF.
$verifypdf = new pdf();
$verifypagecount = $verifypdf->load_pdf($tmpfile);
$verifypdf->Close();
if ($verifypagecount <= 0) {
// No pages were found in the combined PDF.
return $this->mark_combination_failed();
}
// Store the newly created file as a stored_file.
$this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL));
// Note the verified page count.
$this->pagecount = $verifypagecount;
return $this;
}
/**
* Mark the combination attempt as having encountered a permanent failure.
*
* @return $this
*/
protected function mark_combination_failed() {
$this->combinationstatus = self::STATUS_FAILED;
return $this;
}
/**
* Store the combined file in the file_storage API.
*
* @param string $tmpfile The path to the file on disk to be stored.
* @param int $contextid The contextid for the file to be stored under
* @param int $itemid The itemid for the file to be stored under
* @param boolean $partial The combined pdf contains only some of the source files.
* @return $this
*/
protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) {
// Store the file.
$record = $this->get_stored_file_record($contextid, $itemid, $partial);
$fs = get_file_storage();
// Delete existing files first.
$fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
// This was a combined pdf.
$file = $fs->create_file_from_pathname($record, $tmpfile);
$this->set_combined_file($file);
return $this;
}
/**
* Store the empty document file in the file_storage API.
*
* @param int $contextid The contextid for the file to be stored under
* @param int $itemid The itemid for the file to be stored under
* @return $this
*/
protected function store_empty_document($contextid, $itemid) {
// Store the file.
$record = $this->get_stored_file_record($contextid, $itemid);
$fs = get_file_storage();
// Delete existing files first.
$fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
$file = $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64));
$this->pagecount = 1;
$this->set_combined_file($file);
return $this;
}
/**
* Get the total number of pages in the combined document.
*
* If there are no pages, or it is not yet possible to count them a
* value of 0 is returned.
*
* @return int
*/
public function get_page_count() {
if ($this->pagecount) {
return $this->pagecount;
}
$status = $this->get_status();
if ($status === self::STATUS_FAILED) {
// The empty document will be returned.
return 1;
}
if ($status !== self::STATUS_COMPLETE) {
// No pages yet.
return 0;
}
// Load the PDF to determine the page count.
$temparea = make_request_directory();
$tempsrc = $temparea . "/source.pdf";
$this->get_combined_file()->copy_content_to($tempsrc);
$pdf = new pdf();
$pagecount = $pdf->load_pdf($tempsrc);
$pdf->Close();
if ($pagecount <= 0) {
// Something went wrong. Return an empty page count again.
return 0;
}
$this->pagecount = $pagecount;
return $this->pagecount;
}
/**
* Get the total number of documents to be combined.
*
* @return int
*/
public function get_document_count() {
return count($this->sourcefiles);
}
/**
* Helper to fetch the stored_file record.
*
* @param int $contextid The contextid for the file to be stored under
* @param int $itemid The itemid for the file to be stored under
* @param boolean $partial The combined file contains only some of the source files.
* @return stdClass
*/
protected function get_stored_file_record($contextid, $itemid, $partial = false) {
$filearea = document_services::COMBINED_PDF_FILEAREA;
if ($partial) {
$filearea = document_services::PARTIAL_PDF_FILEAREA;
}
return (object) [
'contextid' => $contextid,
'component' => 'assignfeedback_editpdf',
'filearea' => $filearea,
'itemid' => $itemid,
'filepath' => '/',
'filename' => document_services::COMBINED_PDF_FILENAME,
];
}
}
@@ -0,0 +1,79 @@
<?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';
/** @var int draft status, default 1 = true */
public $draft = 1;
/**
* Convert a compatible stdClass into an instance of a comment.
* @param \stdClass $record
*/
public function __construct(\stdClass $record = null) {
if ($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;
}
}
}
}
}
}
@@ -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));
}
}
File diff suppressed because it is too large Load Diff
@@ -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/>.
/**
* An event observer.
*
* @package assignfeedback_editpdf
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignfeedback_editpdf\event;
/**
* An event observer.
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class observer {
/**
* Listen to events and queue the submission for processing.
* @param \mod_assign\event\submission_created $event
*/
public static function submission_created(\mod_assign\event\submission_created $event) {
self::queue_conversion($event);
}
/**
* Listen to events and queue the submission for processing.
* @param \mod_assign\event\submission_updated $event
*/
public static function submission_updated(\mod_assign\event\submission_updated $event) {
self::queue_conversion($event);
}
/**
* Queue the submission for processing.
* @param \mod_assign\event\base $event The submission created/updated event.
*/
protected static function queue_conversion($event) {
$assign = $event->get_assign();
$plugin = $assign->get_feedback_plugin_by_type('editpdf');
if (!$plugin->is_visible() || !$plugin->is_enabled()) {
// The plugin is not enabled on this assignment instance, so nothing should be queued.
return;
}
$data = [
'submissionid' => $event->other['submissionid'],
'submissionattempt' => $event->other['submissionattempt'],
];
$task = new \assignfeedback_editpdf\task\convert_submission;
$task->set_custom_data($data);
\core\task\manager::queue_adhoc_task($task, true);
}
}
@@ -0,0 +1,441 @@
<?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;
}
// Fetch comments ordered by position on the page.
$records = $DB->get_records('assignfeedback_editpdf_cmnt', $params, 'y, x');
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.
if (!($record instanceof comment)) {
$comment = new comment($record);
} else {
$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, 'draft' => 1));
$added = 0;
foreach ($annotations as $record) {
// Force these.
if (!($record instanceof annotation)) {
$annotation = new annotation($record);
} else {
$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));
}
/**
* Copy annotations, comments, pages, and other required content from the source user to the current group member
* being procssed when using applytoall.
*
* @param int|\assign $assignment
* @param \stdClass $grade
* @param int $sourceuserid
* @return bool
*/
public static function copy_drafts_from_to($assignment, $grade, $sourceuserid) {
global $DB;
// Delete any existing annotations and comments from current user.
$DB->delete_records('assignfeedback_editpdf_annot', array('gradeid' => $grade->id));
$DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid' => $grade->id));
// Get gradeid, annotations and comments from sourceuserid.
$sourceusergrade = $assignment->get_user_grade($sourceuserid, true, $grade->attemptnumber);
$annotations = $DB->get_records('assignfeedback_editpdf_annot', array('gradeid' => $sourceusergrade->id, 'draft' => 1));
$comments = $DB->get_records('assignfeedback_editpdf_cmnt', array('gradeid' => $sourceusergrade->id, 'draft' => 1));
$contextid = $assignment->get_context()->id;
$sourceitemid = $sourceusergrade->id;
// Add annotations and comments to current user to generate feedback file.
foreach ($annotations as $annotation) {
$annotation->gradeid = $grade->id;
$DB->insert_record('assignfeedback_editpdf_annot', $annotation);
}
foreach ($comments as $comment) {
$comment->gradeid = $grade->id;
$DB->insert_record('assignfeedback_editpdf_cmnt', $comment);
}
$fs = get_file_storage();
// Copy the stamp files.
self::replace_files_from_to($fs, $contextid, $sourceitemid, $grade->id, document_services::STAMPS_FILEAREA, true);
// Copy the PAGE_IMAGE_FILEAREA files.
self::replace_files_from_to($fs, $contextid, $sourceitemid, $grade->id, document_services::PAGE_IMAGE_FILEAREA);
return true;
}
/**
* Replace the area files in the specified area with those in the source item id.
*
* @param \file_storage $fs The file storage class
* @param int $contextid The ID of the context for the assignment.
* @param int $sourceitemid The itemid to copy from - typically the source grade id.
* @param int $itemid The itemid to copy to - typically the target grade id.
* @param string $area The file storage area.
* @param bool $includesubdirs Whether to copy the content of sub-directories too.
*/
public static function replace_files_from_to($fs, $contextid, $sourceitemid, $itemid, $area, $includesubdirs = false) {
$component = 'assignfeedback_editpdf';
// Remove the existing files within this area.
$fs->delete_area_files($contextid, $component, $area, $itemid);
// Copy the files from the source area.
if ($files = $fs->get_area_files($contextid, $component, $area, $sourceitemid,
"filename", $includesubdirs)) {
foreach ($files as $file) {
$newrecord = new \stdClass();
$newrecord->contextid = $contextid;
$newrecord->itemid = $itemid;
$fs->create_file_from_storedfile($newrecord, $file);
}
}
}
/**
* Delete the draft annotations and comments.
*
* This is intended to be used when the version of the PDF has changed and the annotations
* might not be relevant any more, therefore we should delete them.
*
* @param int $gradeid The grade ID.
* @return bool
*/
public static function delete_draft_content($gradeid) {
global $DB;
$conditions = array('gradeid' => $gradeid, 'draft' => 1);
$result = $DB->delete_records('assignfeedback_editpdf_annot', $conditions);
$result = $result && $DB->delete_records('assignfeedback_editpdf_cmnt', $conditions);
return $result;
}
/**
* Set page rotation value.
* @param int $gradeid grade id.
* @param int $pageno page number.
* @param bool $isrotated whether the page is rotated or not.
* @param string $pathnamehash path name hash
* @param int $degree rotation degree.
* @throws \dml_exception
*/
public static function set_page_rotation($gradeid, $pageno, $isrotated, $pathnamehash, $degree = 0) {
global $DB;
$oldrecord = self::get_page_rotation($gradeid, $pageno);
if ($oldrecord == null) {
$record = new \stdClass();
$record->gradeid = $gradeid;
$record->pageno = $pageno;
$record->isrotated = $isrotated;
$record->pathnamehash = $pathnamehash;
$record->degree = $degree;
$DB->insert_record('assignfeedback_editpdf_rot', $record, false);
} else {
$oldrecord->isrotated = $isrotated;
$oldrecord->pathnamehash = $pathnamehash;
$oldrecord->degree = $degree;
$DB->update_record('assignfeedback_editpdf_rot', $oldrecord, false);
}
}
/**
* Get Page Rotation Value.
* @param int $gradeid grade id.
* @param int $pageno page number.
* @return mixed
* @throws \dml_exception
*/
public static function get_page_rotation($gradeid, $pageno) {
global $DB;
$result = $DB->get_record('assignfeedback_editpdf_rot', array('gradeid' => $gradeid, 'pageno' => $pageno));
return $result;
}
}
+914
View File
@@ -0,0 +1,914 @@
<?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;
use setasign\Fpdi\TcpdfFpdi;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir.'/pdflib.php');
require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/autoload.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 TcpdfFpdi {
/** @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;
/** @var string the fontname used when the PDF being processed */
protected $fontname = 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';
/** Min. width an annotation should have */
const MIN_ANNOTATION_WIDTH = 5;
/** Min. height an annotation should have */
const MIN_ANNOTATION_HEIGHT = 5;
/** Blank PDF file used during error. */
const BLANK_PDF = '/mod/assign/feedback/editpdf/fixtures/blank.pdf';
/** Page image file name prefix*/
const IMAGE_PAGE = 'image_page';
/**
* Get the name of the font to use in generated PDF files.
* If $CFG->pdfexportfont is set - use it, otherwise use "freesans" as this
* open licensed font has wide support for different language charsets.
*
* @return string
*/
private function get_export_font_name() {
$fontname = 'freesans';
if (!empty($this->fontname)) {
$fontname = $this->fontname;
}
return $fontname;
}
/**
* Set font name.
*
* @param string $fontname Font name which is
* @return void
*/
public function set_export_font_name($fontname): void {
$this->fontname = $fontname;
}
/**
* 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) {
raise_memory_limit(MEMORY_EXTRA);
$olddebug = error_reporting(0);
$this->setPageUnit('pt');
$this->setPrintHeader(false);
$this->setPrintFooter(false);
$this->scale = 72.0 / 100.0;
// Use font supporting the widest range of characters.
$this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true);
$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);
error_reporting($olddebug);
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) {
raise_memory_limit(MEMORY_EXTRA);
$olddebug = error_reporting(0);
$this->setPageUnit('pt');
$this->scale = 72.0 / 100.0;
$this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true);
$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;
error_reporting($olddebug);
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);
// Create a page of the required size / orientation.
$this->AddPage($size['orientation'], array($size['width'], $size['height']));
// Prevent new page creation when comments are at the bottom of a page.
$this->setPageOrientation($size['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();
}
}
/**
* Append all comments to the end of the document.
*
* @param array $allcomments All comments, indexed by page number (starting at 0).
* @return array|bool An array of links to comments, or false.
*/
public function append_comments($allcomments) {
if (!$this->filename) {
return false;
}
$this->SetFontSize(12 * $this->scale);
$this->SetMargins(100 * $this->scale, 120 * $this->scale, -1, true);
$this->SetAutoPageBreak(true, 100 * $this->scale);
$this->setHeaderFont(array($this->get_export_font_name(), '', 24 * $this->scale, '', true));
$this->setHeaderMargin(24 * $this->scale);
$this->setHeaderData('', 0, '', get_string('commentindex', 'assignfeedback_editpdf'));
// Add a new page to the document with an appropriate header.
$this->setPrintHeader(true);
$this->AddPage();
// Add the comments.
$commentlinks = array();
foreach ($allcomments as $pageno => $comments) {
foreach ($comments as $index => $comment) {
// Create a link to the current location, which will be added to the marker.
$commentlink = $this->AddLink();
$this->SetLink($commentlink, -1);
$commentlinks[$pageno][$index] = $commentlink;
// Also create a link back to the marker, which will be added here.
$markerlink = $this->AddLink();
$this->SetLink($markerlink, $comment->y * $this->scale, $pageno + 1);
$label = get_string('commentlabel', 'assignfeedback_editpdf', array('pnum' => $pageno + 1, 'cnum' => $index + 1));
$this->Cell(50 * $this->scale, 0, $label, 0, 0, '', false, $markerlink);
$this->MultiCell(0, 0, $comment->rawtext, 0, 'L');
$this->Ln(12 * $this->scale);
}
// Add an extra line break between pages.
$this->Ln(12 * $this->scale);
}
return $commentlinks;
}
/**
* Add a comment marker to the specified page.
*
* @param int $pageno The page number to add markers to (starting at 0).
* @param int $index The comment index.
* @param int $x The x-coordinate of the marker (in pixels).
* @param int $y The y-coordinate of the marker (in pixels).
* @param int $link The link identifier pointing to the full comment text.
* @param string $colour The fill colour of the marker (red, yellow, green, blue, white, clear).
* @return bool Success status.
*/
public function add_comment_marker($pageno, $index, $x, $y, $link, $colour = 'yellow') {
if (!$this->filename) {
return false;
}
$fill = '';
$fillopacity = 0.9;
switch ($colour) {
case 'red':
$fill = 'rgb(249, 181, 179)';
break;
case 'green':
$fill = 'rgb(214, 234, 178)';
break;
case 'blue':
$fill = 'rgb(203, 217, 237)';
break;
case 'white':
$fill = 'rgb(255, 255, 255)';
break;
case 'clear':
$fillopacity = 0;
break;
default: /* Yellow */
$fill = 'rgb(255, 236, 174)';
}
$marker = '@<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 12 12" preserveAspectRatio="xMinYMin meet">' .
'<path d="M11 0H1C.4 0 0 .4 0 1v6c0 .6.4 1 1 1h1v4l4-4h5c.6 0 1-.4 1-1V1c0-.6-.4-1-1-1z" fill="' . $fill . '" ' .
'fill-opacity="' . $fillopacity . '" stroke="rgb(153, 153, 153)" stroke-width="0.5"/></svg>';
$label = get_string('commentlabel', 'assignfeedback_editpdf', array('pnum' => $pageno + 1, 'cnum' => $index + 1));
$x *= $this->scale;
$y *= $this->scale;
$size = 24 * $this->scale;
$this->SetDrawColor(51, 51, 51);
$this->SetFontSize(10 * $this->scale);
$this->setPage($pageno + 1);
// Add the marker image.
$this->ImageSVG($marker, $x - 0.5, $y - 0.5, $size, $size, $link);
// Add the label.
$this->MultiCell($size * 0.95, 0, $label, 0, 'C', false, 1, $x, $y, true, 0, false, true, $size * 0.60, 'M', true);
return true;
}
/**
* 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;
}
$this->SetDrawColor(51, 51, 51);
switch ($colour) {
case 'red':
$this->SetFillColor(249, 181, 179);
break;
case 'green':
$this->SetFillColor(214, 234, 178);
break;
case 'blue':
$this->SetFillColor(203, 217, 237);
break;
case 'white':
$this->SetFillColor(255, 255, 255);
break;
default: /* Yellow */
$this->SetFillColor(255, 236, 174);
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, $type, $path, $imagefolder) {
global $CFG;
if (!$this->filename) {
return false;
}
switch ($colour) {
case 'yellow':
$colourarray = array(255, 207, 53);
break;
case 'green':
$colourarray = array(153, 202, 62);
break;
case 'blue':
$colourarray = array(125, 159, 211);
break;
case 'white':
$colourarray = array(255, 255, 255);
break;
case 'black':
$colourarray = array(51, 51, 51);
break;
default: /* Red */
$colour = 'red';
$colourarray = array(239, 69, 64);
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;
// $rx and $ry should be >= min width and height
if ($rx < self::MIN_ANNOTATION_WIDTH) {
$rx = self::MIN_ANNOTATION_WIDTH;
}
if ($ry < self::MIN_ANNOTATION_HEIGHT) {
$ry = self::MIN_ANNOTATION_HEIGHT;
}
$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);
// Width or height should be >= min width and height
if ($w < self::MIN_ANNOTATION_WIDTH) {
$w = self::MIN_ANNOTATION_WIDTH;
}
if ($h < self::MIN_ANNOTATION_HEIGHT) {
$h = self::MIN_ANNOTATION_HEIGHT;
}
$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);
// width should be >= min width
if ($w < self::MIN_ANNOTATION_WIDTH) {
$w = self::MIN_ANNOTATION_WIDTH;
}
$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;
}
if (!empty($scalepath)) {
$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);
// Stamp is always more than 40px, so no need to check width/height.
$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) {
$olddebug = error_reporting(0);
$this->Output($filename, 'F');
error_reporting($olddebug);
}
/**
* 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 images from the PDF
* @return array Array of filename of the generated images
*/
public function get_images(): array {
$this->precheck_generate_image();
$imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE;
$command = $this->get_command_for_image(-1, $imagefile);
exec($command);
$images = array();
for ($i = 0; $i < $this->pagecount; $i++) {
// Image file is created from 1, so need to change to 0.
$file = $imagefile . ($i + 1) . '.png';
$newfile = $imagefile . $i . '.png';
if (file_exists($file)) {
rename($file, $newfile);
} else {
// Converter added '-' and zerofill for the pagenumber.
$length = strlen($this->pagecount);
$file = $imagefile . '-' . str_pad(($i + 1), $length, '0', STR_PAD_LEFT) . '.png';
if (file_exists($file)) {
rename($file, $newfile);
} else {
$newfile = self::get_error_image($this->imagefolder, $i);
}
}
$images[$i] = basename($newfile);
}
return $images;
}
/**
* 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) {
$this->precheck_generate_image();
$imagefile = $this->imagefolder . '/' . self::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) {
$command = $this->get_command_for_image($pageno, $imagefile);
$output = null;
$result = exec($command, $output);
if (!file_exists($imagefile)) {
$fullerror = '<pre>'.get_string('command', 'assignfeedback_editpdf')."\n";
$fullerror .= $command . "\n\n";
$fullerror .= get_string('result', 'assignfeedback_editpdf')."\n";
$fullerror .= htmlspecialchars($result, ENT_COMPAT) . "\n\n";
$fullerror .= get_string('output', 'assignfeedback_editpdf')."\n";
$fullerror .= htmlspecialchars(implode("\n", $output), ENT_COMPAT) . '</pre>';
throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror);
}
}
return self::IMAGE_PAGE . $pageno . '.png';
}
/**
* Make sure the file name and image folder are ready before generate image.
* @return bool
*/
protected function precheck_generate_image() {
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');
}
return true;
}
/**
* Gets the command to use to extract as image the given $pageno page number
* from a PDF document into the $imagefile file.
* @param int $pageno Page number to extract from document. -1 means for all pages.
* @param string $imagefile Target filename for the PNG image as absolute path.
* @return string The command to use to extract a page as PNG image.
*/
private function get_command_for_image(int $pageno, string $imagefile): string {
global $CFG;
// First, quickest convertion option.
if (!empty($CFG->pathtopdftoppm) && is_executable($CFG->pathtopdftoppm)) {
return $this->get_pdftoppm_command_for_image($pageno, $imagefile);
}
// Otherwise, rely on default behaviour.
return $this->get_gs_command_for_image($pageno, $imagefile);
}
/**
* Gets the pdftoppm command to use to extract as image the given $pageno page number
* from a PDF document into the $imagefile file.
* @param int $pageno Page number to extract from document. -1 means for all pages.
* @param string $imagefile Target filename for the PNG image as absolute path.
* @return string The pdftoppm command to use to extract a page as PNG image.
*/
private function get_pdftoppm_command_for_image(int $pageno, string $imagefile): string {
global $CFG;
$pdftoppmexec = \escapeshellarg($CFG->pathtopdftoppm);
$imageres = \escapeshellarg(100);
$filename = \escapeshellarg($this->filename);
$pagenoinc = \escapeshellarg($pageno + 1);
if ($pageno >= 0) {
// Convert 1 page.
$imagefile = substr($imagefile, 0, -4); // Pdftoppm tool automatically adds extension file.
$frompageno = $pagenoinc;
$topageno = $pagenoinc;
$singlefile = '-singlefile';
} else {
// Convert all pages at once.
$frompageno = 1;
$topageno = $this->pagecount;
$singlefile = '';
}
$imagefilearg = \escapeshellarg($imagefile);
return "$pdftoppmexec -q -r $imageres -f $frompageno -l $topageno -png $singlefile $filename $imagefilearg";
}
/**
* Gets the ghostscript (gs) command to use to extract as image the given $pageno page number
* from a PDF document into the $imagefile file.
* @param int $pageno Page number to extract from document. -1 means for all pages.
* @param string $imagefile Target filename for the PNG image as absolute path.
* @return string The ghostscript (gs) command to use to extract a page as PNG image.
*/
private function get_gs_command_for_image(int $pageno, string $imagefile): string {
global $CFG;
$gsexec = \escapeshellarg($CFG->pathtogs);
$imageres = \escapeshellarg(100);
$imagefilearg = \escapeshellarg($imagefile);
$filename = \escapeshellarg($this->filename);
$pagenoinc = \escapeshellarg($pageno + 1);
if ($pageno >= 0) {
// Convert 1 page.
$firstpage = $pagenoinc;
$lastpage = $pagenoinc;
} else {
// Convert all pages at once.
$imagefilearg = \escapeshellarg($imagefile . '%d.png');
$firstpage = 1;
$lastpage = $this->pagecount;
}
return "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$firstpage -dLastPage=$lastpage ".
"-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename";
}
/**
* 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;
// Copy the stored_file to local disk for checking.
$temparea = make_request_directory();
$tempsrc = $temparea . "/source.pdf";
$file->copy_content_to($tempsrc);
return self::ensure_pdf_file_compatible($tempsrc);
}
/**
* Flatten and convert file using ghostscript then load pdf.
*
* @param string $tempsrc The path to the file on disk.
* @return string path to copy or converted pdf (false == fail)
*/
public static function ensure_pdf_file_compatible($tempsrc) {
global $CFG;
$temparea = make_request_directory();
$tempdst = $temparea . "/target.pdf";
$gsexec = \escapeshellarg($CFG->pathtogs);
$tempdstarg = \escapeshellarg($tempdst);
$tempsrcarg = \escapeshellarg($tempsrc);
$command = "$gsexec -q -sDEVICE=pdfwrite -dPreserveAnnots=false -dSAFER -dBATCH -dNOPAUSE "
. "-sOutputFile=$tempdstarg $tempsrcarg";
exec($command);
if (!file_exists($tempdst)) {
// Something has gone wrong in the conversion.
return false;
}
$pdf = new pdf();
$pagecount = 0;
try {
$pagecount = $pdf->load_pdf($tempdst);
} catch (\Exception $e) {
// PDF was not valid - try running it through ghostscript to clean it up.
$pagecount = 0;
}
$pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
if ($pagecount <= 0) {
// Could not parse the converted pdf.
return false;
}
return $tempdst;
}
/**
* Generate an localised error image for the given pagenumber.
*
* @param string $errorimagefolder path of the folder where error image needs to be created.
* @param int $pageno page number for which error image needs to be created.
*
* @return string File name
* @throws \coding_exception
*/
public static function get_error_image($errorimagefolder, $pageno) {
global $CFG;
$errorfile = $CFG->dirroot . self::BLANK_PDF;
if (!file_exists($errorfile)) {
throw new \coding_exception("Blank PDF not found", "File path" . $errorfile);
}
$tmperrorimagefolder = make_request_directory();
$pdf = new pdf();
$pdf->set_pdf($errorfile);
$pdf->copy_page();
$pdf->add_comment(get_string('errorpdfpage', 'assignfeedback_editpdf'), 250, 300, 200, "red");
$generatedpdf = $tmperrorimagefolder . '/' . 'error.pdf';
$pdf->save_pdf($generatedpdf);
$pdf = new pdf();
$pdf->set_pdf($generatedpdf);
$pdf->set_image_folder($tmperrorimagefolder);
$image = $pdf->get_image(0);
$pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
$newimg = self::IMAGE_PAGE . $pageno . '.png';
copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg);
return $newimg;
}
/**
* 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 \stdClass
*/
public static function test_gs_path($generateimage = true) {
global $CFG;
$ret = (object)array(
'status' => self::GSPATH_OK,
'message' => null,
);
$gspath = $CFG->pathtogs;
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;
}
if (!$generateimage) {
return $ret;
}
$testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf';
if (!file_exists($testfile)) {
$ret->status = self::GSPATH_NOTESTFILE;
return $ret;
}
$testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
$filepath = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png';
// Delete any previous test images, if they exist.
if (file_exists($filepath)) {
unlink($filepath);
}
$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();
}
$pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
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 . '/' . self::IMAGE_PAGE . '0.png';
send_file($testimage, basename($testimage), 0);
die();
}
/**
* This function add an image file to PDF page.
* @param \stored_file $imagestoredfile Image file to be added
*/
public function add_image_page($imagestoredfile) {
$imageinfo = $imagestoredfile->get_imageinfo();
$imagecontent = $imagestoredfile->get_content();
$this->currentpage++;
$template = $this->importPage($this->currentpage);
$size = $this->getTemplateSize($template);
$orientation = 'P';
if ($imageinfo["width"] > $imageinfo["height"]) {
if ($size['width'] < $size['height']) {
$temp = $size['width'];
$size['width'] = $size['height'];
$size['height'] = $temp;
}
$orientation = 'L';
} else if ($imageinfo["width"] < $imageinfo["height"]) {
if ($size['width'] > $size['height']) {
$temp = $size['width'];
$size['width'] = $size['height'];
$size['height'] = $temp;
}
}
$this->SetHeaderMargin(0);
$this->SetFooterMargin(0);
$this->SetMargins(0, 0, 0, true);
$this->setPrintFooter(false);
$this->setPrintHeader(false);
$this->AddPage($orientation, $size);
$this->SetAutoPageBreak(false, 0);
$this->Image('@' . $imagecontent, 0, 0, $size['width'], $size['height'],
'', '', '', false, null, '', false, false, 0);
}
}
@@ -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/>.
/**
* Privacy class for requesting user data.
*
* @package assignfeedback_editpdf
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignfeedback_editpdf\privacy;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/assign/locallib.php');
use \core_privacy\local\metadata\collection;
use \mod_assign\privacy\assignfeedback_provider;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\contextlist;
use \mod_assign\privacy\assign_plugin_request_data;
use \mod_assign\privacy\useridlist;
/**
* Privacy class for requesting user data.
*
* @package assignfeedback_editpdf
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\mod_assign\privacy\assignfeedback_provider,
\mod_assign\privacy\assignfeedback_user_provider {
/**
* Return meta data about this plugin.
*
* @param collection $collection A list of information to add to.
* @return collection Return the collection after adding to it.
*/
public static function get_metadata(collection $collection): collection {
$quickdata = [
'userid' => 'privacy:metadata:userid',
'rawtext' => 'privacy:metadata:rawtextpurpose',
'colour' => 'privacy:metadata:colourpurpose'
];
$collection->add_database_table('assignfeedback_editpdf_quick', $quickdata, 'privacy:metadata:tablepurpose');
$collection->add_subsystem_link('core_files', [], 'privacy:metadata:filepurpose');
$collection->add_subsystem_link('core_fileconverter', [], 'privacy:metadata:conversionpurpose');
return $collection;
}
/**
* No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign
* provider.
*
* @param int $userid The user ID.
* @param contextlist $contextlist The context list.
*/
public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) {
// This uses the assign_grade table.
}
/**
* This also does not need to be filled in as this is already collected in the mod assign provider.
*
* @param useridlist $useridlist A list of user IDs
*/
public static function get_student_user_ids(useridlist $useridlist) {
// Not required.
}
/**
* If you have tables that contain userids and you can generate entries in your tables without creating an
* entry in the assign_grades table then please fill in this method.
*
* @param \core_privacy\local\request\userlist $userlist The userlist object
*/
public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) {
// Not required.
}
/**
* Export all user data for this plugin.
*
* @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful
* information to help with exporting.
*/
public static function export_feedback_user_data(assign_plugin_request_data $exportdata) {
$currentpath = $exportdata->get_subcontext();
$currentpath[] = get_string('privacy:path', 'assignfeedback_editpdf');
$assign = $exportdata->get_assign();
$plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf');
$fileareas = $plugin->get_user_data_file_areas();
$grade = $exportdata->get_pluginobject();
foreach ($fileareas as $filearea => $notused) {
writer::with_context($exportdata->get_context())
->export_area_files($currentpath, 'assignfeedback_editpdf', $filearea, $grade->id);
}
}
/**
* Any call to this method should delete all user data for the context defined in the deletion_criteria.
*
* @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin.
*/
public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) {
$assign = $requestdata->get_assign();
$plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf');
$fileareas = $plugin->get_file_areas();
$fs = get_file_storage();
foreach ($fileareas as $filearea => $notused) {
// Delete pdf files.
$fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_editpdf', $filearea);
}
// Delete entries from the tables.
$plugin->delete_instance();
}
/**
* Calling this function should delete all user data associated with this grade.
*
* @param assign_plugin_request_data $requestdata Data useful for deleting user data.
*/
public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) {
$requestdata->set_userids([$requestdata->get_user()->id]);
$requestdata->populate_submissions_and_grades();
self::delete_feedback_for_grades($requestdata);
}
/**
* Deletes all feedback for the grade ids / userids provided in a context.
* assign_plugin_request_data contains:
* - context
* - assign object
* - grade ids (pluginids)
* - user ids
* @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion.
*/
public static function delete_feedback_for_grades(assign_plugin_request_data $deletedata) {
global $DB;
if (empty($deletedata->get_gradeids())) {
return;
}
$assign = $deletedata->get_assign();
$plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf');
$fileareas = $plugin->get_file_areas();
$fs = get_file_storage();
list($sql, $params) = $DB->get_in_or_equal($deletedata->get_gradeids(), SQL_PARAMS_NAMED);
foreach ($fileareas as $filearea => $notused) {
// Delete pdf files.
$fs->delete_area_files_select($deletedata->get_context()->id, 'assignfeedback_editpdf', $filearea, $sql, $params);
}
// Remove table entries.
$DB->delete_records_select('assignfeedback_editpdf_annot', "gradeid $sql", $params);
$DB->delete_records_select('assignfeedback_editpdf_cmnt', "gradeid $sql", $params);
$DB->delete_records_select('assignfeedback_editpdf_rot', "gradeid $sql", $params);
}
}
@@ -0,0 +1,281 @@
<?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',
'rotateleft' => 'q',
'rotateright' => 'w',
'navigate-page-select' => 'k',
'navigate-next-button' => 'l',
'searchcomments' => 'h',
'expcolcomments' => 'g',
'comment' => 'z',
'commentcolour' => 'x',
'select' => 'c',
'drag' => 'd',
'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 = get_string($tool, 'assignfeedback_editpdf');
if (!empty($accesskey)) {
$alttext->shortcut = '(Alt/Shift-Alt/Ctrl-Option + ' . $accesskey . ')';
} else {
$alttext->shortcut = '';
}
$iconalt = get_string('toolbarbutton', 'assignfeedback_editpdf', $alttext);
$iconhtml = $this->image_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();
$launcheditorstring = $widget->readonly ? get_string('viewfeedbackonline', 'assignfeedback_editpdf') :
get_string('launcheditor', 'assignfeedback_editpdf');
$links = html_writer::link('#', $launcheditorstring, ['id' => $linkid, 'class' => 'd-block mt-2']);
$html .= '<input type="hidden" name="assignfeedback_editpdf_haschanges" value="false"/>';
$html .= html_writer::div($links, 'visibleifjs');
$header = get_string('pluginname', 'assignfeedback_editpdf');
$body = '';
// Create the page navigation.
$navigation1 = '';
$navigation2 = '';
$navigation3 = '';
// 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';
}
$iconshortcut = $this->get_shortcut('navigate-previous-button');
$iconalt = get_string('navigateprevious', 'assignfeedback_editpdf', $iconshortcut);
$iconhtml = $this->image_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')));
$navigation1 .= html_writer::tag('select', null, array('disabled'=>'true',
'aria-label' => get_string('gotopage', 'assignfeedback_editpdf'), 'class'=>'navigate-page-select',
'accesskey' => $this->get_shortcut('navigate-page-select')));
$iconshortcut = $this->get_shortcut('navigate-next-button');
$iconalt = get_string('navigatenext', 'assignfeedback_editpdf', $iconshortcut);
$iconhtml = $this->image_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'));
$navigation3 .= $this->render_toolbar_button('comment_expcol', 'expcolcomments', $this->get_shortcut('expcolcomments'));
$navigation3 = html_writer::div($navigation3, 'navigation-expcol', array('role' => 'navigation'));
$rotationtools = '';
if (!$widget->readonly) {
$rotationtools .= $this->render_toolbar_button('rotate_left', 'rotateleft', $this->get_shortcut('rotateleft'));
$rotationtools .= $this->render_toolbar_button('rotate_right', 'rotateright', $this->get_shortcut('rotateright'));
$rotationtools = html_writer::div($rotationtools, 'toolbar', array('role' => 'toolbar'));
}
$toolbargroup = '';
$clearfix = html_writer::div('', 'clearfix');
if (!$widget->readonly) {
// Comments.
$toolbar1 = '';
$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 = '';
$toolbar2 .= $this->render_toolbar_button('drag', 'drag', $this->get_shortcut('drag'));
$toolbar2 .= $this->render_toolbar_button('select', 'select', $this->get_shortcut('select'));
$toolbar2 = html_writer::div($toolbar2, 'toolbar', array('role' => 'toolbar'));
// Other Tools.
$toolbar3 = '';
$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 = '';
$toolbar4 .= $this->render_toolbar_button('stamp', 'stamp', $this->get_shortcut('stamp'));
$toolbar4 .= $this->render_toolbar_button('background_colour_clear', 'currentstamp', $this->get_shortcut('currentstamp'));
$toolbar4 = html_writer::div($toolbar4, 'toolbar', array('role'=>'toolbar'));
// Add toolbars to toolbar_group in order of display, and float the toolbar_group right.
$toolbars = $rotationtools . $toolbar1 . $toolbar2 . $toolbar3 . $toolbar4;
$toolbargroup = html_writer::div($toolbars, 'toolbar_group', ['role' => 'toolbar']);
}
$pageheader = html_writer::div($navigation1 .
$navigation2 .
$navigation3 .
$toolbargroup .
$clearfix,
'pageheader');
$body = $pageheader;
// Loading progress bar.
$progressbar = html_writer::div('', 'bar', array('style' => 'width: 0%'));
$progressbar = html_writer::div($progressbar, 'progress progress-info progress-striped active',
array('title' => get_string('loadingeditor', 'assignfeedback_editpdf'),
'role'=> 'progressbar', 'aria-valuenow' => 0, 'aria-valuemin' => 0,
'aria-valuemax' => 100));
$progressbarlabel = html_writer::div(get_string('generatingpdf', 'assignfeedback_editpdf'),
'progressbarlabel');
$loading = html_writer::div($progressbar . $progressbarlabel, 'loading');
$canvas = html_writer::div($loading, 'drawingcanvas');
$canvas = html_writer::div($canvas, 'drawingregion');
// Place for messages, but no warnings displayed yet.
$changesmessage = html_writer::div('', 'warningmessages');
$canvas .= $changesmessage;
$infoicon = $this->image_icon('i/info', '');
$infomessage = html_writer::div($infoicon, 'infoicon');
$canvas .= $infomessage;
$body .= $canvas;
$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',
'pagenumber',
'partialwarning',
'draftchangessaved'
), 'assignfeedback_editpdf');
return $html;
}
}
@@ -0,0 +1,103 @@
<?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/>.
/**
* Bump submission timemodified for conversions that are stale.
*
* @package assignfeedback_editpdf
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameronball@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignfeedback_editpdf\task;
use core\task\adhoc_task;
/**
* Adhoc task to bump the submission timemodified associated with a stale conversion.
*
* @package assignfeedback_editpdf
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameronball@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class bump_submission_for_stale_conversions extends adhoc_task {
/**
* Run the task.
*/
public function execute() {
global $DB;
// Used to only get records after whenever document conversion was enabled for this site.
$earliestconversion = $DB->get_record_sql("SELECT MIN(timecreated) AS min
FROM {files}
WHERE filearea = 'documentconversion'");
if (isset($earliestconversion->min)) {
['sql' => $extensionsql, 'params' => $extensionparams] = array_reduce(
['doc', 'docx', 'rtf', 'xls', 'xlsx', 'ppt', 'pptx', 'html', 'odt', 'ods', 'png', 'jpg', 'txt', 'gif'],
function(array $c, string $ext) use ($DB): array {
return [
'sql' => $c['sql'] . ($c['sql'] ? ' OR ' : '') . $DB->sql_like('f1.filename', ':' . $ext),
'params' => $c['params'] + [$ext => '%.' . $ext]
];
},
['sql' => '', 'params' => []]
);
// A converted file has its filename set to the contenthash of the file it converted.
// Find all files in the relevant file areas for which there is no corresponding
// file with the contenthash as the file name.
//
// Also check if the file has a greater modified time than the submission, if it does
// that means it is both stale (as per the above) and will never be reconverted.
$sql = "SELECT f3.id, f3.timemodified as fmodified, asu.id as submissionid
FROM {files} f1
LEFT JOIN {files} f2 ON f1.contenthash = f2.filename
AND f2.component = 'core' AND f2.filearea = 'documentconversion'
JOIN {assign_submission} asu ON asu.id = f1.itemid
JOIN {assign_grades} asg ON asg.userid = asu.userid AND asg.assignment = asu.assignment
JOIN {files} f3 ON f3.itemid = asg.id
WHERE f1.filearea = 'submission_files'
AND f3.timecreated >= :earliest
AND ($extensionsql)
AND f2.filename IS NULL
AND f3.component = 'assignfeedback_editpdf'
AND f3.filearea = 'combined'
AND f3.filename = 'combined.pdf'
AND f3.timemodified >= asu.timemodified";
$submissionstobump = $DB->get_records_sql($sql, ['earliest' => $earliestconversion->min] + $extensionparams);
foreach ($submissionstobump as $submission) {
// Set the submission modified time to one second later than the
// converted files modified time, this will cause assign to reconvert
// everything and delete the old files when the assignment grader is
// viewed. See get_page_images_for_attempt in document_services.php.
$newmodified = $submission->fmodified + 1;
$record = (object)[
'id' => $submission->submissionid,
'timemodified' => $newmodified
];
mtrace('Set submission ' . $submission->submissionid . ' timemodified to ' . $newmodified);
$DB->update_record('assign_submission', $record);
}
}
}
}
@@ -0,0 +1,136 @@
<?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/>.
namespace assignfeedback_editpdf\task;
use core\task\adhoc_task;
use core\task\manager;
use assignfeedback_editpdf\document_services;
use assignfeedback_editpdf\combined_document;
use assignfeedback_editpdf\pdf;
use context_module;
use moodle_exception;
use assign;
/**
* An adhoc task to convert submissions to pdf in the background.
*
* @copyright 2022 Mikhail Golenkov <mikhailgolenkov@catalyst-au.net>
* @package assignfeedback_editpdf
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class convert_submission extends adhoc_task {
/**
* Run the task.
*/
public function execute() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
$data = $this->get_custom_data();
$submission = $DB->get_record('assign_submission', ['id' => $data->submissionid], '*', IGNORE_MISSING);
if (!$submission) {
mtrace('Submission no longer exists');
return;
}
// Early exit if ghostscript isn't correctly configured.
$result = pdf::test_gs_path(false);
if ($result->status !== pdf::GSPATH_OK) {
$statusstring = get_string('test_' . $result->status, 'assignfeedback_editpdf');
throw new moodle_exception('pathtogserror', 'assignfeedback_editpdf', '', $statusstring, $result->status);
}
$cm = get_coursemodule_from_instance('assign', $submission->assignment, 0, false, MUST_EXIST);
$context = context_module::instance($cm->id);
$assign = new assign($context, null, null);
if ($submission->userid) {
$users = [$submission->userid];
} else {
$users = [];
$members = $assign->get_submission_group_members($submission->groupid, true);
foreach ($members as $member) {
$users[] = $member->id;
}
}
$conversionrequirespolling = false;
foreach ($users as $userid) {
mtrace('Converting submission for user id ' . $userid);
// If the assignment is not vieweable, we should not try to convert the documents
// for this submission, as it will cause the adhoc task to fail with a permission
// error.
//
// Comments on MDL-56810 indicate that submission conversion should not be attempted
// if the submission is not viewable due to the user not being enrolled.
if (!$assign->can_view_submission($userid)) {
continue;
}
// Note: Before MDL-71468, the scheduled task version of this
// task would stop attempting to poll the conversion after a
// configured number of attempts were made to poll it, see:
//
// mod/assign/feedback/editpdf/classes/task/convert_submissions.php@MOODLE_400_STABLE
//
// This means that currently this adhoc task, if it fails, will retry forever. But
// the fail-delay mechanism will ensure that it eventually only tries once per day.
//
// See MDL-75457 for details on re-implementing the conversionattemptlimit.
//
// Also note: This code must not be in a try/catch - an exception needs to be thrown to
// allow the task API to mark the task as failed and update its faildelay. Using
// manager::adhoc_task_failed in the catch block will not work, as the task API
// will later assume the task completed successfully (as no exception was thrown) and
// complete it (removing it from the adhoc task queue).
$combineddocument = document_services::get_combined_pdf_for_attempt($assign, $userid, $data->submissionattempt);
switch ($combineddocument->get_status()) {
case combined_document::STATUS_READY:
case combined_document::STATUS_READY_PARTIAL:
case combined_document::STATUS_PENDING_INPUT:
// The document has not been converted yet or is somehow still ready.
$conversionrequirespolling = true;
continue 2;
case combined_document::STATUS_FAILED:
// Although STATUS_FAILED indicates a "permanent error" it should be possible
// in some cases to fix the underlying problem, allowing the conversion to
// complete. So we throw an exception here, allowing the adhoc task to retry.
//
// Currently this can result in the task trying indefinitely (although it will
// settle on trying once per day due to the faildelay exponential backoff)
// however once the conversionattepmtlimit is re-implemented in MDL-75457 the
// task will eventually get dropped.
throw new \moodle_exception('documentcombinationfailed');
}
document_services::get_page_images_for_attempt($assign, $userid, $data->submissionattempt, false);
document_services::get_page_images_for_attempt($assign, $userid, $data->submissionattempt, true);
}
if ($conversionrequirespolling) {
mtrace('Conversion still in progress. Requeueing self to check again.');
$task = new self;
$task->set_custom_data($data);
$task->set_next_run_time(time() + MINSECS);
manager::queue_adhoc_task($task);
} else {
mtrace('The document has been successfully converted');
}
}
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* Cleans up orphaned feedback pdf files and table entries.
*
* @package assignfeedback_editpdf
* @copyright 2022 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignfeedback_editpdf\task;
use core\task\adhoc_task;
/**
* Cleans up orphaned feedback pdf files and table entries.
*
* @package assignfeedback_editpdf
* @copyright 2022 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class remove_orphaned_editpdf_files extends adhoc_task {
/**
* Run the task.
*/
public function execute() {
$this->remove_files_and_entries();
$this->remove_rotated_table_entries();
}
/**
* Removes edit pdf feedback files and table entries that have been orphaned.
*/
private function remove_files_and_entries(): void {
global $DB;
// Patiently remove all orphaned temporary pdf files.
$sql = "SELECT DISTINCT f.contextid, f.component, f.filearea, f.itemid
FROM {files} f
LEFT JOIN {assign_grades} g ON g.id = f.itemid
WHERE f.component = :assigneditpdf
AND NOT (filearea = :stamps AND f.itemid = 0)
AND g.id IS NULL";
$params = ['assigneditpdf' => 'assignfeedback_editpdf', 'stamps' => 'stamps'];
$results = $DB->get_recordset_sql($sql, $params);
foreach ($results as $record) {
$fs = get_file_storage();
$fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
}
$results->close();
}
/**
* Removes orphaned entries in the feedback edit pdf rotation table.
*/
private function remove_rotated_table_entries(): void {
global $DB;
$rotatesql = "SELECT er.id AS erid
FROM {assignfeedback_editpdf_rot} er
LEFT JOIN {assign_grades} g ON g.id = er.gradeid
WHERE g.id IS NULL";
$DB->delete_records_subquery('assignfeedback_editpdf_rot', 'id', 'erid', $rotatesql);
}
}
@@ -0,0 +1,71 @@
<?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;
}
}