Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains the ingest manager for the assignfeedback_editpdf plugin
  19   *
  20   * @package   assignfeedback_editpdf
  21   * @copyright 2012 Davo Smith
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace assignfeedback_editpdf;
  26  
  27  use DOMDocument;
  28  
  29  /**
  30   * Functions for generating the annotated pdf.
  31   *
  32   * This class controls the ingest of student submission files to a normalised
  33   * PDF 1.4 document with all submission files concatinated together. It also
  34   * provides the functions to generate a downloadable pdf with all comments and
  35   * annotations embedded.
  36   * @copyright 2012 Davo Smith
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class document_services {
  40  
  41      /** Compoment name */
  42      const COMPONENT = "assignfeedback_editpdf";
  43      /** File area for generated pdf */
  44      const FINAL_PDF_FILEAREA = 'download';
  45      /** File area for combined pdf */
  46      const COMBINED_PDF_FILEAREA = 'combined';
  47      /** File area for partial combined pdf */
  48      const PARTIAL_PDF_FILEAREA = 'partial';
  49      /** File area for importing html */
  50      const IMPORT_HTML_FILEAREA = 'importhtml';
  51      /** File area for page images */
  52      const PAGE_IMAGE_FILEAREA = 'pages';
  53      /** File area for readonly page images */
  54      const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages';
  55      /** File area for the stamps */
  56      const STAMPS_FILEAREA = 'stamps';
  57      /** Filename for combined pdf */
  58      const COMBINED_PDF_FILENAME = 'combined.pdf';
  59      /**  Temporary place to save JPG Image to PDF file */
  60      const TMP_JPG_TO_PDF_FILEAREA = 'tmp_jpg_to_pdf';
  61      /**  Temporary place to save (Automatically) Rotated JPG FILE */
  62      const TMP_ROTATED_JPG_FILEAREA = 'tmp_rotated_jpg';
  63      /** Hash of blank pdf */
  64      const BLANK_PDF_HASH = '4c803c92c71f21b423d13de570c8a09e0a31c718';
  65  
  66      /** Base64 encoded blank pdf. This is the most reliable/fastest way to generate a blank pdf. */
  67      const BLANK_PDF_BASE64 = <<<EOD
  68  JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
  69  Y29kZT4+CnN0cmVhbQp4nDPQM1Qo5ypUMFAwALJMLU31jBQsTAz1LBSKUrnCtRTyuAIVAIcdB3IK
  70  ZW5kc3RyZWFtCmVuZG9iagoKMyAwIG9iago0MgplbmRvYmoKCjUgMCBvYmoKPDwKPj4KZW5kb2Jq
  71  Cgo2IDAgb2JqCjw8L0ZvbnQgNSAwIFIKL1Byb2NTZXRbL1BERi9UZXh0XQo+PgplbmRvYmoKCjEg
  72  MCBvYmoKPDwvVHlwZS9QYWdlL1BhcmVudCA0IDAgUi9SZXNvdXJjZXMgNiAwIFIvTWVkaWFCb3hb
  73  MCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+
  74  L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNl
  75  cyA2IDAgUgovTWVkaWFCb3hbIDAgMCA1OTUgODQyIF0KL0tpZHNbIDEgMCBSIF0KL0NvdW50IDE+
  76  PgplbmRvYmoKCjcgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDQgMCBSCi9PcGVuQWN0aW9u
  77  WzEgMCBSIC9YWVogbnVsbCBudWxsIDBdCi9MYW5nKGVuLUFVKQo+PgplbmRvYmoKCjggMCBvYmoK
  78  PDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAw
  79  NEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAwNjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzQwMDJFMDAz
  80  ND4KL0NyZWF0aW9uRGF0ZShEOjIwMTYwMjI2MTMyMzE0KzA4JzAwJyk+PgplbmRvYmoKCnhyZWYK
  81  MCA5CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDIyNiAwMDAwMCBuIAowMDAwMDAwMDE5IDAw
  82  MDAwIG4gCjAwMDAwMDAxMzIgMDAwMDAgbiAKMDAwMDAwMDM2OCAwMDAwMCBuIAowMDAwMDAwMTUx
  83  IDAwMDAwIG4gCjAwMDAwMDAxNzMgMDAwMDAgbiAKMDAwMDAwMDQ2NiAwMDAwMCBuIAowMDAwMDAw
  84  NTYyIDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA5L1Jvb3QgNyAwIFIKL0luZm8gOCAwIFIKL0lE
  85  IFsgPEJDN0REQUQwRDQyOTQ1OTQ2OUU4NzJCMjI1ODUyNkU4Pgo8QkM3RERBRDBENDI5NDU5NDY5
  86  RTg3MkIyMjU4NTI2RTg+IF0KL0RvY0NoZWNrc3VtIC9BNTYwMEZCMDAzRURCRTg0MTNBNTk3RTZF
  87  MURDQzJBRgo+PgpzdGFydHhyZWYKNzM2CiUlRU9GCg==
  88  EOD;
  89  
  90      /**
  91       * This function will take an int or an assignment instance and
  92       * return an assignment instance. It is just for convenience.
  93       * @param int|\assign $assignment
  94       * @return \assign
  95       */
  96      private static function get_assignment_from_param($assignment) {
  97          global $CFG;
  98  
  99          require_once($CFG->dirroot . '/mod/assign/locallib.php');
 100  
 101          if (!is_object($assignment)) {
 102              $cm = get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST);
 103              $context = \context_module::instance($cm->id);
 104  
 105              $assignment = new \assign($context, null, null);
 106          }
 107          return $assignment;
 108      }
 109  
 110      /**
 111       * Get a hash that will be unique and can be used in a path name.
 112       * @param int|\assign $assignment
 113       * @param int $userid
 114       * @param int $attemptnumber (-1 means latest attempt)
 115       */
 116      private static function hash($assignment, $userid, $attemptnumber) {
 117          if (is_object($assignment)) {
 118              $assignmentid = $assignment->get_instance()->id;
 119          } else {
 120              $assignmentid = $assignment;
 121          }
 122          return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber);
 123      }
 124  
 125      /**
 126       * Use a DOM parser to accurately replace images with their alt text.
 127       * @param string $html
 128       * @return string New html with no image tags.
 129       */
 130      protected static function strip_images($html) {
 131          // Load HTML and suppress any parsing errors (DOMDocument->loadHTML() does not current support HTML5 tags).
 132          $dom = new DOMDocument();
 133          libxml_use_internal_errors(true);
 134          $dom->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $html);
 135          libxml_clear_errors();
 136  
 137          // Find all img tags.
 138          if ($imgnodes = $dom->getElementsByTagName('img')) {
 139              // Replace img nodes with the img alt text without overriding DOM elements.
 140              for ($i = ($imgnodes->length - 1); $i >= 0; $i--) {
 141                  $imgnode = $imgnodes->item($i);
 142                  $alt = ($imgnode->hasAttribute('alt')) ? ' [ ' . $imgnode->getAttribute('alt') . ' ] ' : ' ';
 143                  $textnode = $dom->createTextNode($alt);
 144  
 145                  $imgnode->parentNode->replaceChild($textnode, $imgnode);
 146              }
 147          }
 148          $count = 1;
 149          return str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", "", $dom->saveHTML(), $count);
 150      }
 151  
 152      /**
 153       * This function will search for all files that can be converted
 154       * and concatinated into a PDF (1.4) - for any submission plugin
 155       * for this students attempt.
 156       *
 157       * @param int|\assign $assignment
 158       * @param int $userid
 159       * @param int $attemptnumber (-1 means latest attempt)
 160       * @return combined_document
 161       */
 162      protected static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) {
 163          global $USER, $DB;
 164  
 165          $assignment = self::get_assignment_from_param($assignment);
 166  
 167          // Capability checks.
 168          if (!$assignment->can_view_submission($userid)) {
 169              throw new \moodle_exception('nopermission');
 170          }
 171  
 172          $files = array();
 173  
 174          if ($assignment->get_instance()->teamsubmission) {
 175              $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber);
 176          } else {
 177              $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
 178          }
 179          $user = $DB->get_record('user', array('id' => $userid));
 180  
 181          // User has not submitted anything yet.
 182          if (!$submission) {
 183              return new combined_document();
 184          }
 185  
 186          $fs = get_file_storage();
 187          $converter = new \core_files\converter();
 188          // Ask each plugin for it's list of files.
 189          foreach ($assignment->get_submission_plugins() as $plugin) {
 190              if ($plugin->is_enabled() && $plugin->is_visible()) {
 191                  $pluginfiles = $plugin->get_files($submission, $user);
 192                  foreach ($pluginfiles as $filename => $file) {
 193                      if ($file instanceof \stored_file) {
 194                          $mimetype = $file->get_mimetype();
 195                          // PDF File, no conversion required.
 196                          if ($mimetype === 'application/pdf') {
 197                              $files[$filename] = $file;
 198                          } else if ($plugin->allow_image_conversion() && $mimetype === "image/jpeg") {
 199                              // Rotates image based on the EXIF value.
 200                              list ($rotateddata, $size) = $file->rotate_image();
 201                              if ($rotateddata) {
 202                                  $file = self::save_rotated_image_file($assignment, $userid, $attemptnumber,
 203                                      $rotateddata, $filename);
 204                              }
 205                              // Save as PDF file if there is no available converter.
 206                              if (!$converter->can_convert_format_to('jpg', 'pdf')) {
 207                                  $pdffile = self::save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size);
 208                                  if ($pdffile) {
 209                                      $files[$filename] = $pdffile;
 210                                  }
 211                              }
 212                          }
 213                          // The file has not been converted to PDF, try to convert it to PDF.
 214                          if (!isset($files[$filename])
 215                              && $convertedfile = $converter->start_conversion($file, 'pdf')) {
 216                              $files[$filename] = $convertedfile;
 217                          }
 218                      } else if ($converter->can_convert_format_to('html', 'pdf')) {
 219                          // Create a tmp stored_file from this html string.
 220                          $file = reset($file);
 221                          // Strip image tags, because they will not be resolvable.
 222                          $file = self::strip_images($file);
 223                          $record = new \stdClass();
 224                          $record->contextid = $assignment->get_context()->id;
 225                          $record->component = 'assignfeedback_editpdf';
 226                          $record->filearea = self::IMPORT_HTML_FILEAREA;
 227                          $record->itemid = $submission->id;
 228                          $record->filepath = '/';
 229                          $record->filename = $plugin->get_type() . '-' . $filename;
 230  
 231                          $htmlfile = $fs->get_file($record->contextid,
 232                                  $record->component,
 233                                  $record->filearea,
 234                                  $record->itemid,
 235                                  $record->filepath,
 236                                  $record->filename);
 237  
 238                          $newhash = sha1($file);
 239  
 240                          // If the file exists, and the content hash doesn't match, remove it.
 241                          if ($htmlfile && $newhash !== $htmlfile->get_contenthash()) {
 242                              $htmlfile->delete();
 243                              $htmlfile = false;
 244                          }
 245  
 246                          // If the file doesn't exist, or if it was removed above, create a new one.
 247                          if (!$htmlfile) {
 248                              $htmlfile = $fs->create_file_from_string($record, $file);
 249                          }
 250  
 251                          $convertedfile = $converter->start_conversion($htmlfile, 'pdf');
 252  
 253                          if ($convertedfile) {
 254                              $files[$filename] = $convertedfile;
 255                          }
 256                      }
 257                  }
 258              }
 259          }
 260          $combineddocument = new combined_document();
 261          $combineddocument->set_source_files($files);
 262  
 263          return $combineddocument;
 264      }
 265  
 266      /**
 267       * Fetch the current combined document ready for state checking.
 268       *
 269       * @param int|\assign $assignment
 270       * @param int $userid
 271       * @param int $attemptnumber (-1 means latest attempt)
 272       * @return combined_document
 273       */
 274      public static function get_combined_document_for_attempt($assignment, $userid, $attemptnumber) {
 275          global $USER, $DB;
 276  
 277          $assignment = self::get_assignment_from_param($assignment);
 278  
 279          // Capability checks.
 280          if (!$assignment->can_view_submission($userid)) {
 281              throw new \moodle_exception('nopermissiontoaccesspage', 'error');
 282          }
 283  
 284          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 285          if ($assignment->get_instance()->teamsubmission) {
 286              $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber);
 287          } else {
 288              $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
 289          }
 290  
 291          $contextid = $assignment->get_context()->id;
 292          $component = 'assignfeedback_editpdf';
 293          $filearea = self::COMBINED_PDF_FILEAREA;
 294          $partialfilearea = self::PARTIAL_PDF_FILEAREA;
 295          $itemid = $grade->id;
 296          $filepath = '/';
 297          $filename = self::COMBINED_PDF_FILENAME;
 298          $fs = get_file_storage();
 299  
 300          $partialpdf = $fs->get_file($contextid, $component, $partialfilearea, $itemid, $filepath, $filename);
 301          if (!empty($partialpdf)) {
 302              $combinedpdf = $partialpdf;
 303          } else {
 304              $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
 305          }
 306  
 307          if ($combinedpdf && $submission) {
 308              if ($combinedpdf->get_timemodified() < $submission->timemodified) {
 309                  // The submission has been updated since the PDF was generated.
 310                  $combinedpdf = false;
 311              } else if ($combinedpdf->get_contenthash() == self::BLANK_PDF_HASH) {
 312                  // The PDF is for a blank page.
 313                  $combinedpdf = false;
 314              }
 315          }
 316  
 317          if (empty($combinedpdf)) {
 318              // The combined PDF does not exist yet. Return the list of files to be combined.
 319              return self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber);
 320          } else {
 321              // The combined PDF aleady exists. Return it in a new combined_document object.
 322              $combineddocument = new combined_document();
 323              return $combineddocument->set_combined_file($combinedpdf);
 324          }
 325      }
 326  
 327      /**
 328       * This function return the combined pdf for all valid submission files.
 329       *
 330       * @param int|\assign $assignment
 331       * @param int $userid
 332       * @param int $attemptnumber (-1 means latest attempt)
 333       * @return combined_document
 334       */
 335      public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
 336          $document = self::get_combined_document_for_attempt($assignment, $userid, $attemptnumber);
 337  
 338          if ($document->get_status() === combined_document::STATUS_COMPLETE) {
 339              // The combined document is already ready.
 340              return $document;
 341          } else {
 342              // Attempt to combined the files in the document.
 343              $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 344              $document->combine_files($assignment->get_context()->id, $grade->id);
 345              return $document;
 346          }
 347      }
 348  
 349      /**
 350       * This function will return the number of pages of a pdf.
 351       *
 352       * @param int|\assign $assignment
 353       * @param int $userid
 354       * @param int $attemptnumber (-1 means latest attempt)
 355       * @param bool $readonly When true we get the number of pages for the readonly version.
 356       * @return int number of pages
 357       */
 358      public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 359          global $CFG;
 360  
 361          require_once($CFG->libdir . '/pdflib.php');
 362  
 363          $assignment = self::get_assignment_from_param($assignment);
 364  
 365          if (!$assignment->can_view_submission($userid)) {
 366              throw new \moodle_exception('nopermission');
 367          }
 368  
 369          // When in readonly we can return the number of images in the DB because they should already exist,
 370          // if for some reason they do not, then we proceed as for the normal version.
 371          if ($readonly) {
 372              $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 373              $fs = get_file_storage();
 374              $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf',
 375                  self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/');
 376              $pagecount = count($files);
 377              if ($pagecount > 0) {
 378                  return $pagecount;
 379              }
 380          }
 381  
 382          // Get a combined pdf file from all submitted pdf files.
 383          $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 384          return $document->get_page_count();
 385      }
 386  
 387      /**
 388       * This function will generate and return a list of the page images from a pdf.
 389       * @param int|\assign $assignment
 390       * @param int $userid
 391       * @param int $attemptnumber (-1 means latest attempt)
 392       * @param bool $resetrotation check if need to reset page rotation information
 393       * @return array(stored_file)
 394       */
 395      protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation = true) {
 396          global $CFG;
 397  
 398          require_once($CFG->libdir . '/pdflib.php');
 399  
 400          $assignment = self::get_assignment_from_param($assignment);
 401  
 402          if (!$assignment->can_view_submission($userid)) {
 403              throw new \moodle_exception('nopermission');
 404          }
 405  
 406          // Need to generate the page images - first get a combined pdf.
 407          $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 408  
 409          $status = $document->get_status();
 410          if ($status === combined_document::STATUS_FAILED) {
 411              throw new \moodle_exception('Could not generate combined pdf.');
 412          } else if ($status === combined_document::STATUS_PENDING_INPUT) {
 413              // The conversion is still in progress.
 414              return [];
 415          }
 416  
 417          $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
 418          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 419  
 420          $document->get_combined_file()->copy_content_to($combined); // Copy the file.
 421  
 422          $pdf = new pdf();
 423  
 424          $pdf->set_image_folder($tmpdir);
 425          $pagecount = $pdf->set_pdf($combined);
 426  
 427          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 428  
 429          $record = new \stdClass();
 430          $record->contextid = $assignment->get_context()->id;
 431          $record->component = 'assignfeedback_editpdf';
 432          $record->filearea = self::PAGE_IMAGE_FILEAREA;
 433          $record->itemid = $grade->id;
 434          $record->filepath = '/';
 435          $fs = get_file_storage();
 436  
 437          // Remove the existing content of the filearea.
 438          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 439  
 440          $files = array();
 441          $images = $pdf->get_images();
 442          for ($i = 0; $i < $pagecount; $i++) {
 443              try {
 444                  if (empty($images[$i])) {
 445                      throw new \moodle_exception('error image');
 446                  }
 447                  $image = $images[$i];
 448                  if (!$resetrotation) {
 449                      $pagerotation = page_editor::get_page_rotation($grade->id, $i);
 450                      $degree = !empty($pagerotation) ? $pagerotation->degree : 0;
 451                      if ($degree != 0) {
 452                          $filepath = $tmpdir . '/' . $image;
 453                          $imageresource = imagecreatefrompng($filepath);
 454                          $content = imagerotate($imageresource, $degree, 0);
 455                          imagepng($content, $filepath);
 456                      }
 457                  }
 458              } catch (\moodle_exception $e) {
 459                  // We catch only moodle_exception here as other exceptions indicate issue with setup not the pdf.
 460                  $image = pdf::get_error_image($tmpdir, $i);
 461              }
 462              $record->filename = basename($image);
 463              $files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
 464              @unlink($tmpdir . '/' . $image);
 465              // Set page rotation default value.
 466              if (!empty($files[$i])) {
 467                  if ($resetrotation) {
 468                      page_editor::set_page_rotation($grade->id, $i, false, $files[$i]->get_pathnamehash());
 469                  }
 470              }
 471          }
 472          $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
 473  
 474          @unlink($combined);
 475          @rmdir($tmpdir);
 476  
 477          return $files;
 478      }
 479  
 480      /**
 481       * This function returns a list of the page images from a pdf.
 482       *
 483       * The readonly version is different than the normal one. The readonly version contains a copy
 484       * of the pages in the state they were when the PDF was annotated, by doing so we prevent the
 485       * the pages that are displayed to change as soon as the submission changes.
 486       *
 487       * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible
 488       * that we do not find any readonly version of the pages. In that case, we will get the normal
 489       * pages and copy them to the readonly area. This ensures that the pages will remain in that
 490       * state until the submission is updated. When the normal files do not exist, we throw an exception
 491       * because the readonly pages should only ever be displayed after a teacher has annotated the PDF,
 492       * they would not exist until they do.
 493       *
 494       * @param int|\assign $assignment
 495       * @param int $userid
 496       * @param int $attemptnumber (-1 means latest attempt)
 497       * @param bool $readonly If true, then we are requesting the readonly version.
 498       * @return array(stored_file)
 499       */
 500      public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 501          global $DB;
 502  
 503          $assignment = self::get_assignment_from_param($assignment);
 504  
 505          if (!$assignment->can_view_submission($userid)) {
 506              throw new \moodle_exception('nopermission');
 507          }
 508  
 509          if ($assignment->get_instance()->teamsubmission) {
 510              $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber);
 511          } else {
 512              $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
 513          }
 514          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 515  
 516          $contextid = $assignment->get_context()->id;
 517          $component = 'assignfeedback_editpdf';
 518          $itemid = $grade->id;
 519          $filepath = '/';
 520          $filearea = self::PAGE_IMAGE_FILEAREA;
 521  
 522          $fs = get_file_storage();
 523  
 524          // If we are after the readonly pages...
 525          if ($readonly) {
 526              $filearea = self::PAGE_IMAGE_READONLY_FILEAREA;
 527              if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) {
 528                  // We have a problem here, we were supposed to find the files.
 529                  // Attempt to re-generate the pages from the combined images.
 530                  self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
 531                  self::copy_pages_to_readonly_area($assignment, $grade);
 532              }
 533          }
 534  
 535          $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 536  
 537          $pages = array();
 538          $resetrotation = false;
 539          if (!empty($files)) {
 540              $first = reset($files);
 541              $pagemodified = $first->get_timemodified();
 542              // Check that we don't just have a single blank page. The hash of a blank page image can vary with
 543              // the version of ghostscript used, so we need to examine the combined pdf it was generated from.
 544              $blankpage = false;
 545              if (!$readonly && count($files) == 1) {
 546                  $pdfarea = self::COMBINED_PDF_FILEAREA;
 547                  $pdfname = self::COMBINED_PDF_FILENAME;
 548                  if ($pdf = $fs->get_file($contextid, $component, $pdfarea, $itemid, $filepath, $pdfname)) {
 549                      // The combined pdf may have a different hash if it has been regenerated since the page
 550                      // image was created. However if this is the case the page image will be stale anyway.
 551                      if ($pdf->get_contenthash() == self::BLANK_PDF_HASH || $pagemodified < $pdf->get_timemodified()) {
 552                          $blankpage = true;
 553                      }
 554                  }
 555              }
 556              if (!$readonly && ($pagemodified < $submission->timemodified || $blankpage)) {
 557                  // Image files are stale, we need to regenerate them, except in readonly mode.
 558                  // We also need to remove the draft annotations and comments associated with this attempt.
 559                  $fs->delete_area_files($contextid, $component, $filearea, $itemid);
 560                  page_editor::delete_draft_content($itemid);
 561                  $files = array();
 562                  $resetrotation = true;
 563              } else {
 564  
 565                  // Need to reorder the files following their name.
 566                  // because get_directory_files() return a different order than generate_page_images_for_attempt().
 567                  foreach ($files as $file) {
 568                      // Extract the page number from the file name image_pageXXXX.png.
 569                      preg_match('/page([\d]+)\./', $file->get_filename(), $matches);
 570                      if (empty($matches) or !is_numeric($matches[1])) {
 571                          throw new \coding_exception("'" . $file->get_filename()
 572                              . "' file hasn't the expected format filename: image_pageXXXX.png.");
 573                      }
 574                      $pagenumber = (int)$matches[1];
 575  
 576                      // Save the page in the ordered array.
 577                      $pages[$pagenumber] = $file;
 578                  }
 579                  ksort($pages);
 580              }
 581          }
 582  
 583          // This should never happen, there should be a version of the pages available
 584          // whenever we are requesting the readonly version.
 585          if (empty($pages) && $readonly) {
 586              throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id);
 587          }
 588  
 589          // There are two situations where the number of page images generated does not
 590          // match the number of pages in the PDF:
 591          //
 592          // 1. The document conversion adhoc task was interrupted somehow (node died, solar flare, etc)
 593          // 2. The submission has been updated by the student
 594          //
 595          // In the case of 1. we need to regenerate the pages, see MDL-66626.
 596          // In the case of 2. we should do nothing, see MDL-45580.
 597          //
 598          // To differentiate between 1. and 2. we can check if the submission has been modified since the
 599          // pages were generated. If it has, then we're in situation 2.
 600          $totalpagesforattempt = self::page_number_for_attempt($assignment, $userid, $attemptnumber, false);
 601          $submissionmodified = isset($pagemodified) && $submission->timemodified > $pagemodified;
 602          if (empty($pages) || (count($pages) != $totalpagesforattempt && !$submissionmodified)) {
 603              $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation);
 604          }
 605  
 606          return $pages;
 607      }
 608  
 609      /**
 610       * This function returns sensible filename for a feedback file.
 611       * @param int|\assign $assignment
 612       * @param int $userid
 613       * @param int $attemptnumber (-1 means latest attempt)
 614       * @return string
 615       */
 616      protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) {
 617          global $DB;
 618  
 619          $assignment = self::get_assignment_from_param($assignment);
 620  
 621          $groupmode = groups_get_activity_groupmode($assignment->get_course_module());
 622          $groupname = '';
 623          if ($groupmode) {
 624              $groupid = groups_get_activity_group($assignment->get_course_module(), true);
 625              $groupname = groups_get_group_name($groupid).'-';
 626          }
 627          if ($groupname == '-') {
 628              $groupname = '';
 629          }
 630          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 631          $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
 632  
 633          if ($assignment->is_blind_marking()) {
 634              $prefix = $groupname . get_string('participant', 'assign');
 635              $prefix = str_replace('_', ' ', $prefix);
 636              $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
 637          } else {
 638              $prefix = $groupname . fullname($user);
 639              $prefix = str_replace('_', ' ', $prefix);
 640              $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
 641          }
 642          $prefix .= $grade->attemptnumber;
 643  
 644          return $prefix . '.pdf';
 645      }
 646  
 647      /**
 648       * This function takes the combined pdf and embeds all the comments and annotations.
 649       *
 650       * This also moves the annotations and comments from drafts to not drafts. And it will
 651       * copy all the images stored to the readonly area, so that they can be viewed online, and
 652       * not be overwritten when a new submission is sent.
 653       *
 654       * @param int|\assign $assignment
 655       * @param int $userid
 656       * @param int $attemptnumber (-1 means latest attempt)
 657       * @return \stored_file
 658       */
 659      public static function generate_feedback_document($assignment, $userid, $attemptnumber) {
 660          global $CFG;
 661  
 662          $assignment = self::get_assignment_from_param($assignment);
 663  
 664          if (!$assignment->can_view_submission($userid)) {
 665              throw new \moodle_exception('nopermission');
 666          }
 667          if (!$assignment->can_grade()) {
 668              throw new \moodle_exception('nopermission');
 669          }
 670  
 671          // Need to generate the page images - first get a combined pdf.
 672          $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 673  
 674          $status = $document->get_status();
 675          if ($status === combined_document::STATUS_FAILED) {
 676              throw new \moodle_exception('Could not generate combined pdf.');
 677          } else if ($status === combined_document::STATUS_PENDING_INPUT) {
 678              // The conversion is still in progress.
 679              return false;
 680          }
 681  
 682          $file = $document->get_combined_file();
 683  
 684          $tmpdir = make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber));
 685          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 686          $file->copy_content_to($combined); // Copy the file.
 687  
 688          $pdf = new pdf();
 689  
 690          // Set fontname from course setting if it's enabled.
 691          if (!empty($CFG->enablepdfexportfont)) {
 692              $fontlist = $pdf->get_export_fontlist();
 693              // Load font from course if it's more than 1.
 694              if (count($fontlist) > 1) {
 695                  $course = $assignment->get_course();
 696                  if (!empty($course->pdfexportfont)) {
 697                      $pdf->set_export_font_name($course->pdfexportfont);
 698                  }
 699              } else {
 700                  $pdf->set_export_font_name(current($fontlist));
 701              }
 702          }
 703  
 704          $fs = get_file_storage();
 705          $stamptmpdir = make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber));
 706          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 707          // Copy any new stamps to this instance.
 708          if ($files = $fs->get_area_files($assignment->get_context()->id,
 709                                           'assignfeedback_editpdf',
 710                                           'stamps',
 711                                           $grade->id,
 712                                           "filename",
 713                                           false)) {
 714              foreach ($files as $file) {
 715                  $filename = $stamptmpdir . '/' . $file->get_filename();
 716                  $file->copy_content_to($filename); // Copy the file.
 717              }
 718          }
 719  
 720          $pagecount = $pdf->set_pdf($combined);
 721          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 722          page_editor::release_drafts($grade->id);
 723  
 724          $allcomments = array();
 725  
 726          for ($i = 0; $i < $pagecount; $i++) {
 727              $pagerotation = page_editor::get_page_rotation($grade->id, $i);
 728              $pagemargin = $pdf->getBreakMargin();
 729              $autopagebreak = $pdf->getAutoPageBreak();
 730              if (empty($pagerotation) || !$pagerotation->isrotated) {
 731                  $pdf->copy_page();
 732              } else {
 733                  $rotatedimagefile = $fs->get_file_by_hash($pagerotation->pathnamehash);
 734                  if (empty($rotatedimagefile)) {
 735                      $pdf->copy_page();
 736                  } else {
 737                      $pdf->add_image_page($rotatedimagefile);
 738                  }
 739              }
 740  
 741              $comments = page_editor::get_comments($grade->id, $i, false);
 742              $annotations = page_editor::get_annotations($grade->id, $i, false);
 743  
 744              if (!empty($comments)) {
 745                  $allcomments[$i] = $comments;
 746              }
 747  
 748              foreach ($annotations as $annotation) {
 749                  $pdf->add_annotation($annotation->x,
 750                                       $annotation->y,
 751                                       $annotation->endx,
 752                                       $annotation->endy,
 753                                       $annotation->colour,
 754                                       $annotation->type,
 755                                       $annotation->path,
 756                                       $stamptmpdir);
 757              }
 758              $pdf->SetAutoPageBreak($autopagebreak, $pagemargin);
 759              $pdf->setPageMark();
 760          }
 761  
 762          if (!empty($allcomments)) {
 763              // Append all comments to the end of the document.
 764              $links = $pdf->append_comments($allcomments);
 765              // Add the comment markers with links.
 766              foreach ($allcomments as $pageno => $comments) {
 767                  foreach ($comments as $index => $comment) {
 768                      $pdf->add_comment_marker($comment->pageno, $index, $comment->x, $comment->y, $links[$pageno][$index],
 769                              $comment->colour);
 770                  }
 771              }
 772          }
 773  
 774          fulldelete($stamptmpdir);
 775  
 776          $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber);
 777          $filename = clean_param($filename, PARAM_FILE);
 778  
 779          $generatedpdf = $tmpdir . '/' . $filename;
 780          $pdf->save_pdf($generatedpdf);
 781  
 782          $record = new \stdClass();
 783  
 784          $record->contextid = $assignment->get_context()->id;
 785          $record->component = 'assignfeedback_editpdf';
 786          $record->filearea = self::FINAL_PDF_FILEAREA;
 787          $record->itemid = $grade->id;
 788          $record->filepath = '/';
 789          $record->filename = $filename;
 790  
 791          // Only keep one current version of the generated pdf.
 792          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 793  
 794          $file = $fs->create_file_from_pathname($record, $generatedpdf);
 795  
 796          // Cleanup.
 797          @unlink($generatedpdf);
 798          @unlink($combined);
 799          @rmdir($tmpdir);
 800  
 801          self::copy_pages_to_readonly_area($assignment, $grade);
 802  
 803          return $file;
 804      }
 805  
 806      /**
 807       * Copy the pages image to the readonly area.
 808       *
 809       * @param int|\assign $assignment The assignment.
 810       * @param \stdClass $grade The grade record.
 811       * @return void
 812       */
 813      public static function copy_pages_to_readonly_area($assignment, $grade) {
 814          $fs = get_file_storage();
 815          $assignment = self::get_assignment_from_param($assignment);
 816          $contextid = $assignment->get_context()->id;
 817          $component = 'assignfeedback_editpdf';
 818          $itemid = $grade->id;
 819  
 820          // Get all the pages.
 821          $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid);
 822          if (empty($originalfiles)) {
 823              // Nothing to do here...
 824              return;
 825          }
 826  
 827          // Delete the old readonly files.
 828          $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid);
 829  
 830          // Do the copying.
 831          foreach ($originalfiles as $originalfile) {
 832              $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile);
 833          }
 834      }
 835  
 836      /**
 837       * This function returns the generated pdf (if it exists).
 838       * @param int|\assign $assignment
 839       * @param int $userid
 840       * @param int $attemptnumber (-1 means latest attempt)
 841       * @return \stored_file
 842       */
 843      public static function get_feedback_document($assignment, $userid, $attemptnumber) {
 844  
 845          $assignment = self::get_assignment_from_param($assignment);
 846  
 847          if (!$assignment->can_view_submission($userid)) {
 848              throw new \moodle_exception('nopermission');
 849          }
 850  
 851          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 852  
 853          $contextid = $assignment->get_context()->id;
 854          $component = 'assignfeedback_editpdf';
 855          $filearea = self::FINAL_PDF_FILEAREA;
 856          $itemid = $grade->id;
 857          $filepath = '/';
 858  
 859          $fs = get_file_storage();
 860          $files = $fs->get_area_files($contextid,
 861                                       $component,
 862                                       $filearea,
 863                                       $itemid,
 864                                       "itemid, filepath, filename",
 865                                       false);
 866          if ($files) {
 867              return reset($files);
 868          }
 869          return false;
 870      }
 871  
 872      /**
 873       * This function deletes the generated pdf for a student.
 874       * @param int|\assign $assignment
 875       * @param int $userid
 876       * @param int $attemptnumber (-1 means latest attempt)
 877       * @return bool
 878       */
 879      public static function delete_feedback_document($assignment, $userid, $attemptnumber) {
 880  
 881          $assignment = self::get_assignment_from_param($assignment);
 882  
 883          if (!$assignment->can_view_submission($userid)) {
 884              throw new \moodle_exception('nopermission');
 885          }
 886          if (!$assignment->can_grade()) {
 887              throw new \moodle_exception('nopermission');
 888          }
 889  
 890          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 891  
 892          $contextid = $assignment->get_context()->id;
 893          $component = 'assignfeedback_editpdf';
 894          $filearea = self::FINAL_PDF_FILEAREA;
 895          $itemid = $grade->id;
 896  
 897          $fs = get_file_storage();
 898          return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
 899      }
 900  
 901      /**
 902       * Get All files in a File area
 903       * @param int|\assign $assignment Assignment
 904       * @param int $userid User ID
 905       * @param int $attemptnumber Attempt Number
 906       * @param string $filearea File Area
 907       * @param string $filepath File Path
 908       * @return array
 909       */
 910      private static function get_files($assignment, $userid, $attemptnumber, $filearea, $filepath = '/') {
 911          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 912          $itemid = $grade->id;
 913          $contextid = $assignment->get_context()->id;
 914          $component = self::COMPONENT;
 915          $fs = get_file_storage();
 916          $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 917          return $files;
 918      }
 919  
 920      /**
 921       * Save file.
 922       * @param int|\assign $assignment Assignment
 923       * @param int $userid User ID
 924       * @param int $attemptnumber Attempt Number
 925       * @param string $filearea File Area
 926       * @param string $newfilepath File Path
 927       * @param string $storedfilepath stored file path
 928       * @return \stored_file
 929       * @throws \file_exception
 930       * @throws \stored_file_creation_exception
 931       */
 932      private static function save_file($assignment, $userid, $attemptnumber, $filearea, $newfilepath, $storedfilepath = '/') {
 933          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 934          $itemid = $grade->id;
 935          $contextid = $assignment->get_context()->id;
 936  
 937          $record = new \stdClass();
 938          $record->contextid = $contextid;
 939          $record->component = self::COMPONENT;
 940          $record->filearea = $filearea;
 941          $record->itemid = $itemid;
 942          $record->filepath = $storedfilepath;
 943          $record->filename = basename($newfilepath);
 944  
 945          $fs = get_file_storage();
 946  
 947          $oldfile = $fs->get_file($record->contextid, $record->component, $record->filearea,
 948              $record->itemid, $record->filepath, $record->filename);
 949  
 950          if ($oldfile) {
 951              $newhash = \file_storage::hash_from_path($newfilepath);
 952              if ($newhash === $oldfile->get_contenthash()) {
 953                  // Use existing file if contenthash match.
 954                  return $oldfile;
 955              }
 956              // Delete existing file.
 957              $oldfile->delete();
 958          }
 959  
 960          return $fs->create_file_from_pathname($record, $newfilepath);
 961      }
 962  
 963      /**
 964       * This function rotate a page, and mark the page as rotated.
 965       * @param int|\assign $assignment Assignment
 966       * @param int $userid User ID
 967       * @param int $attemptnumber Attempt Number
 968       * @param int $index Index of Current Page
 969       * @param bool $rotateleft To determine whether the page is rotated left or right.
 970       * @return null|\stored_file return rotated File
 971       * @throws \coding_exception
 972       * @throws \file_exception
 973       * @throws \moodle_exception
 974       * @throws \stored_file_creation_exception
 975       */
 976      public static function rotate_page($assignment, $userid, $attemptnumber, $index, $rotateleft) {
 977          $assignment = self::get_assignment_from_param($assignment);
 978          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 979          // Check permission.
 980          if (!$assignment->can_view_submission($userid)) {
 981              throw new \moodle_exception('nopermission');
 982          }
 983  
 984          $filearea = self::PAGE_IMAGE_FILEAREA;
 985          $files = self::get_files($assignment, $userid, $attemptnumber, $filearea);
 986          if (!empty($files)) {
 987              foreach ($files as $file) {
 988                  preg_match('/' . pdf::IMAGE_PAGE . '([\d]+)\./', $file->get_filename(), $matches);
 989                  if (empty($matches) or !is_numeric($matches[1])) {
 990                      throw new \coding_exception("'" . $file->get_filename()
 991                          . "' file hasn't the expected format filename: image_pageXXXX.png.");
 992                  }
 993                  $pagenumber = (int)$matches[1];
 994  
 995                  if ($pagenumber == $index) {
 996                      $source = imagecreatefromstring($file->get_content());
 997                      $pagerotation = page_editor::get_page_rotation($grade->id, $index);
 998                      $degree = empty($pagerotation) ? 0 : $pagerotation->degree;
 999                      if ($rotateleft) {
1000                          $content = imagerotate($source, 90, 0);
1001                          $degree = ($degree + 90) % 360;
1002                      } else {
1003                          $content = imagerotate($source, -90, 0);
1004                          $degree = ($degree - 90) % 360;
1005                      }
1006                      $filename = $matches[0].'png';
1007                      $tmpdir = make_temp_directory(self::COMPONENT . '/' . self::PAGE_IMAGE_FILEAREA . '/'
1008                          . self::hash($assignment, $userid, $attemptnumber));
1009                      $tempfile = $tmpdir . '/' . time() . '_' . $filename;
1010                      imagepng($content, $tempfile);
1011  
1012                      $filearea = self::PAGE_IMAGE_FILEAREA;
1013                      $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
1014  
1015                      unlink($tempfile);
1016                      rmdir($tmpdir);
1017                      imagedestroy($source);
1018                      imagedestroy($content);
1019                      $file->delete();
1020                      if (!empty($newfile)) {
1021                          page_editor::set_page_rotation($grade->id, $pagenumber, true, $newfile->get_pathnamehash(), $degree);
1022                      }
1023                      return $newfile;
1024                  }
1025              }
1026          }
1027          return null;
1028      }
1029  
1030      /**
1031       * Convert jpg file to pdf file
1032       * @param int|\assign $assignment Assignment
1033       * @param int $userid User ID
1034       * @param int $attemptnumber Attempt Number
1035       * @param \stored_file $file file to save
1036       * @param null|array $size size of image
1037       * @return \stored_file
1038       * @throws \file_exception
1039       * @throws \stored_file_creation_exception
1040       */
1041      private static function save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size=null) {
1042          // Temporary file.
1043          $filename = $file->get_filename();
1044          $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
1045              . self::TMP_JPG_TO_PDF_FILEAREA . DIRECTORY_SEPARATOR
1046              . self::hash($assignment, $userid, $attemptnumber));
1047          $tempfile = $tmpdir . DIRECTORY_SEPARATOR . $filename . ".pdf";
1048          // Determine orientation.
1049          $orientation = 'P';
1050          if (!empty($size['width']) && !empty($size['height'])) {
1051              if ($size['width'] > $size['height']) {
1052                  $orientation = 'L';
1053              }
1054          }
1055          // Save JPG image to PDF file.
1056          $pdf = new pdf();
1057          $pdf->SetHeaderMargin(0);
1058          $pdf->SetFooterMargin(0);
1059          $pdf->SetMargins(0, 0, 0, true);
1060          $pdf->setPrintFooter(false);
1061          $pdf->setPrintHeader(false);
1062          $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
1063          $pdf->AddPage($orientation);
1064          $pdf->SetAutoPageBreak(false);
1065          // Width has to be define here to fit into A4 page. Otherwise the image will be inserted with original size.
1066          if ($orientation == 'P') {
1067              $pdf->Image('@' . $file->get_content(), 0, 0, 210);
1068          } else {
1069              $pdf->Image('@' . $file->get_content(), 0, 0, 297);
1070          }
1071          $pdf->setPageMark();
1072          $pdf->save_pdf($tempfile);
1073          $filearea = self::TMP_JPG_TO_PDF_FILEAREA;
1074          $pdffile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
1075          if (file_exists($tempfile)) {
1076              unlink($tempfile);
1077              rmdir($tmpdir);
1078          }
1079          return $pdffile;
1080      }
1081  
1082      /**
1083       * Save rotated image data to file.
1084       * @param int|\assign $assignment Assignment
1085       * @param int $userid User ID
1086       * @param int $attemptnumber Attempt Number
1087       * @param resource $rotateddata image data to save
1088       * @param string $filename name of the image file
1089       * @return \stored_file
1090       * @throws \file_exception
1091       * @throws \stored_file_creation_exception
1092       */
1093      private static function save_rotated_image_file($assignment, $userid, $attemptnumber, $rotateddata, $filename) {
1094          $filearea = self::TMP_ROTATED_JPG_FILEAREA;
1095          $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
1096              . $filearea . DIRECTORY_SEPARATOR
1097              . self::hash($assignment, $userid, $attemptnumber));
1098          $tempfile = $tmpdir . DIRECTORY_SEPARATOR . basename($filename);
1099          imagejpeg($rotateddata, $tempfile);
1100          $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
1101          if (file_exists($tempfile)) {
1102              unlink($tempfile);
1103              rmdir($tmpdir);
1104          }
1105          return $newfile;
1106      }
1107  
1108  }