MDL-61537 assignfeedback_editpdf: Rotate PDF page
authorNathan Nguyen <nathannguyen@catalyst-au.net>
Fri, 15 Feb 2019 04:15:22 +0000 (15:15 +1100)
committerNathan Nguyen <ext-nnguy@monash.edu>
Fri, 15 Mar 2019 04:02:33 +0000 (15:02 +1100)
Add page rotation feature.

20 files changed:
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/feedback/editpdf/classes/privacy/provider.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/db/install.xml
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/pix/rotate_left.svg [new file with mode: 0644]
mod/assign/feedback/editpdf/pix/rotate_right.svg [new file with mode: 0644]
mod/assign/feedback/editpdf/version.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js

index fa1e21d..c940e87 100644 (file)
@@ -225,5 +225,26 @@ if ($action === 'pollconversions') {
     $result = $result && page_editor::unrelease_drafts($grade->id);
     echo json_encode($result);
     die();
+} else if ($action == 'rotatepage') {
+    require_capability('mod/assign:grade', $context);
+    $response = new stdClass();
+    $index = required_param('index', PARAM_INT);
+    $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+    $rotateleft = required_param('rotateleft', PARAM_BOOL);
+    $filearea = document_services::PAGE_IMAGE_FILEAREA;
+    $pagefile = document_services::rotate_page($assignment, $userid, $attemptnumber, $index, $rotateleft);
+    $page = new stdClass();
+    $page->url = moodle_url::make_pluginfile_url($context->id, document_services::COMPONENT, $filearea,
+        $grade->id, '/', $pagefile->get_filename())->out();
+    if ($imageinfo = $pagefile->get_imageinfo()) {
+        $page->width = $imageinfo['width'];
+        $page->height = $imageinfo['height'];
+    } else {
+        $page->width = 0;
+        $page->height = 0;
+    }
+    $response = (object)['page' => $page];
+    echo json_encode($response);
+    die();
 }
 
index 4fe8cc6..35fef67 100644 (file)
@@ -48,19 +48,25 @@ class backup_assignfeedback_editpdf_subplugin extends backup_subplugin {
         $subpluginelementannotation = new backup_nested_element('annotation', null, array('gradeid', 'pageno', 'type', 'x', 'y', 'endx', 'endy', 'colour', 'path', 'draft'));
         $subpluginelementcomments = new backup_nested_element('feedback_editpdf_comments');
         $subpluginelementcomment = new backup_nested_element('comment', null, array('gradeid', 'pageno', 'x', 'y', 'width', 'rawtext', 'colour', 'draft'));
+        $subpluginelementrotation = new backup_nested_element('feedback_editpdf_rotation');
+        $subpluginelementpagerotation = new backup_nested_element('pagerotation', null,
+            array('gradeid', 'pageno', 'pathnamehash', 'isrotated', 'degree'));
 
         // Connect XML elements into the tree.
         $subplugin->add_child($subpluginwrapper);
         $subpluginelementannotations->add_child($subpluginelementannotation);
         $subpluginelementcomments->add_child($subpluginelementcomment);
+        $subpluginelementrotation->add_child($subpluginelementpagerotation);
         $subpluginwrapper->add_child($subpluginelementfiles);
         $subpluginwrapper->add_child($subpluginelementannotations);
         $subpluginwrapper->add_child($subpluginelementcomments);
+        $subpluginwrapper->add_child($subpluginelementrotation);
 
         // Set source to populate the data.
         $subpluginelementfiles->set_source_sql('SELECT id AS gradeid from {assign_grades} where id = :gradeid', array('gradeid' => backup::VAR_PARENTID));
         $subpluginelementannotation->set_source_table('assignfeedback_editpdf_annot', array('gradeid' => backup::VAR_PARENTID));
         $subpluginelementcomment->set_source_table('assignfeedback_editpdf_cmnt', array('gradeid' => backup::VAR_PARENTID));
+        $subpluginelementpagerotation->set_source_table('assignfeedback_editpdf_rot', array('gradeid' => backup::VAR_PARENTID));
         // We only need to backup the files in the final pdf area, and the readonly page images - the others can be regenerated.
         $subpluginelementfiles->annotate_files('assignfeedback_editpdf',
             \assignfeedback_editpdf\document_services::FINAL_PDF_FILEAREA, 'gradeid');
index c520e2b..048de0a 100644 (file)
@@ -57,6 +57,11 @@ class restore_assignfeedback_editpdf_subplugin extends restore_subplugin {
         $elepath = $this->get_pathfor('/feedback_editpdf_annotations/annotation');
         $paths[] = new restore_path_element($elename, $elepath);
 
+        // Rotation details.
+        $elename = $this->get_namefor('pagerotation');
+        $elepath = $this->get_pathfor('/feedback_editpdf_rotation/pagerotation');
+        $paths[] = new restore_path_element($elename, $elepath);
+
         return $paths;
     }
 
@@ -109,4 +114,15 @@ class restore_assignfeedback_editpdf_subplugin extends restore_subplugin {
 
     }
 
+    /**
+     * Processes one /feedback_editpdf_rotation/pagerotation element
+     * @param mixed $data
+     */
+    public function process_assignfeedback_editpdf_pagerotation($data) {
+        global $DB;
+        $data = (object)$data;
+        $oldgradeid = $data->gradeid;
+        $data->gradeid = $this->get_mappingid('grade', $oldgradeid);
+        $DB->insert_record('assignfeedback_editpdf_rot', $data);
+    }
 }
index 45b4083..6060236 100644 (file)
@@ -38,6 +38,8 @@ use DOMDocument;
  */
 class document_services {
 
+    /** Compoment name */
+    const COMPONENT = "assignfeedback_editpdf";
     /** File area for generated pdf */
     const FINAL_PDF_FILEAREA = 'download';
     /** File area for combined pdf */
@@ -263,7 +265,6 @@ EOD;
             $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
         }
 
-
         $contextid = $assignment->get_context()->id;
         $component = 'assignfeedback_editpdf';
         $filearea = self::COMBINED_PDF_FILEAREA;
@@ -365,9 +366,10 @@ EOD;
      * @param int|\assign $assignment
      * @param int $userid
      * @param int $attemptnumber (-1 means latest attempt)
+     * @param bool $resetrotation check if need to reset page rotation information
      * @return array(stored_file)
      */
-    protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) {
+    protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation = true) {
         global $CFG;
 
         require_once($CFG->libdir . '/pdflib.php');
@@ -416,6 +418,16 @@ EOD;
         for ($i = 0; $i < $pagecount; $i++) {
             try {
                 $image = $pdf->get_image($i);
+                if (!$resetrotation) {
+                    $pagerotation = page_editor::get_page_rotation($grade->id, $i);
+                    $degree = !empty($pagerotation) ? $pagerotation->degree : 0;
+                    if ($degree != 0) {
+                        $filepath = $tmpdir . '/' . $image;
+                        $imageresource = imagecreatefrompng($filepath);
+                        $content = imagerotate($imageresource, $degree, 0);
+                        imagepng($content, $filepath);
+                    }
+                }
             } catch (\moodle_exception $e) {
                 // We catch only moodle_exception here as other exceptions indicate issue with setup not the pdf.
                 $image = pdf::get_error_image($tmpdir, $i);
@@ -423,6 +435,12 @@ EOD;
             $record->filename = basename($image);
             $files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
             @unlink($tmpdir . '/' . $image);
+            // Set page rotation default value.
+            if (!empty($files[$i])) {
+                if ($resetrotation) {
+                    page_editor::set_page_rotation($grade->id, $i, false, $files[$i]->get_pathnamehash());
+                }
+            }
         }
         $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
 
@@ -490,6 +508,7 @@ EOD;
         $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 
         $pages = array();
+        $resetrotation = false;
         if (!empty($files)) {
             $first = reset($files);
             $pagemodified = $first->get_timemodified();
@@ -513,11 +532,12 @@ EOD;
                 $fs->delete_area_files($contextid, $component, $filearea, $itemid);
                 page_editor::delete_draft_content($itemid);
                 $files = array();
+                $resetrotation = true;
             } else {
 
                 // Need to reorder the files following their name.
                 // because get_directory_files() return a different order than generate_page_images_for_attempt().
-                foreach($files as $file) {
+                foreach ($files as $file) {
                     // Extract the page number from the file name image_pageXXXX.png.
                     preg_match('/page([\d]+)\./', $file->get_filename(), $matches);
                     if (empty($matches) or !is_numeric($matches[1])) {
@@ -539,7 +559,7 @@ EOD;
                 // whenever we are requesting the readonly version.
                 throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id);
             }
-            $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
+            $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation);
         }
 
         return $pages;
@@ -648,7 +668,20 @@ EOD;
         $allcomments = array();
 
         for ($i = 0; $i < $pagecount; $i++) {
-            $pdf->copy_page();
+            $pagerotation = page_editor::get_page_rotation($grade->id, $i);
+            $pagemargin = $pdf->getBreakMargin();
+            $autopagebreak = $pdf->getAutoPageBreak();
+            if (empty($pagerotation) || !$pagerotation->isrotated) {
+                $pdf->copy_page();
+            } else {
+                $rotatedimagefile = $fs->get_file_by_hash($pagerotation->pathnamehash);
+                if (empty($rotatedimagefile)) {
+                    $pdf->copy_page();
+                } else {
+                    $pdf->add_image_page($rotatedimagefile);
+                }
+            }
+
             $comments = page_editor::get_comments($grade->id, $i, false);
             $annotations = page_editor::get_annotations($grade->id, $i, false);
 
@@ -666,6 +699,8 @@ EOD;
                                      $annotation->path,
                                      $stamptmpdir);
             }
+            $pdf->SetAutoPageBreak($autopagebreak, $pagemargin);
+            $pdf->setPageMark();
         }
 
         if (!empty($allcomments)) {
@@ -688,7 +723,6 @@ EOD;
         $generatedpdf = $tmpdir . '/' . $filename;
         $pdf->save_pdf($generatedpdf);
 
-
         $record = new \stdClass();
 
         $record->contextid = $assignment->get_context()->id;
@@ -698,7 +732,6 @@ EOD;
         $record->filepath = '/';
         $record->filename = $filename;
 
-
         // Only keep one current version of the generated pdf.
         $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 
@@ -809,4 +842,129 @@ EOD;
         return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
     }
 
+    /**
+     * Get All files in a File area
+     * @param int|\assign $assignment Assignment
+     * @param int $userid User ID
+     * @param int $attemptnumber Attempt Number
+     * @param string $filearea File Area
+     * @param string $filepath File Path
+     * @return array
+     */
+    private static function get_files($assignment, $userid, $attemptnumber, $filearea, $filepath = '/') {
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        $itemid = $grade->id;
+        $contextid = $assignment->get_context()->id;
+        $component = self::COMPONENT;
+        $fs = get_file_storage();
+        $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
+        return $files;
+    }
+
+    /**
+     * Save file.
+     * @param int|\assign $assignment Assignment
+     * @param int $userid User ID
+     * @param int $attemptnumber Attempt Number
+     * @param string $filearea File Area
+     * @param string $newfilepath File Path
+     * @param string $storedfilepath stored file path
+     * @return \stored_file
+     * @throws \file_exception
+     * @throws \stored_file_creation_exception
+     */
+    private static function save_file($assignment, $userid, $attemptnumber, $filearea, $newfilepath, $storedfilepath = '/') {
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        $itemid = $grade->id;
+        $contextid = $assignment->get_context()->id;
+
+        $record = new \stdClass();
+        $record->contextid = $contextid;
+        $record->component = self::COMPONENT;
+        $record->filearea = $filearea;
+        $record->itemid = $itemid;
+        $record->filepath = $storedfilepath;
+        $record->filename = basename($newfilepath);
+
+        $fs = get_file_storage();
+
+        $oldfile = $fs->get_file($record->contextid, $record->component, $record->filearea,
+            $record->itemid, $record->filepath, $record->filename);
+
+        $newhash = sha1($newfilepath);
+
+        // Delete old file if exists.
+        if ($oldfile && $newhash !== $oldfile->get_contenthash()) {
+            $oldfile->delete();
+        }
+
+        return $fs->create_file_from_pathname($record, $newfilepath);
+    }
+
+    /**
+     * This function rotate a page, and mark the page as rotated.
+     * @param int|\assign $assignment Assignment
+     * @param int $userid User ID
+     * @param int $attemptnumber Attempt Number
+     * @param int $index Index of Current Page
+     * @param bool $rotateleft To determine whether the page is rotated left or right.
+     * @return null|\stored_file return rotated File
+     * @throws \coding_exception
+     * @throws \file_exception
+     * @throws \moodle_exception
+     * @throws \stored_file_creation_exception
+     */
+    public static function rotate_page($assignment, $userid, $attemptnumber, $index, $rotateleft) {
+        $assignment = self::get_assignment_from_param($assignment);
+        $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+        // Check permission.
+        if (!$assignment->can_view_submission($userid)) {
+            print_error('nopermission');
+        }
+
+        $filearea = self::PAGE_IMAGE_FILEAREA;
+        $files = self::get_files($assignment, $userid, $attemptnumber, $filearea);
+        if (!empty($files)) {
+            foreach ($files as $file) {
+                preg_match('/' . pdf::IMAGE_PAGE . '([\d]+)\./', $file->get_filename(), $matches);
+                if (empty($matches) or !is_numeric($matches[1])) {
+                    throw new \coding_exception("'" . $file->get_filename()
+                        . "' file hasn't the expected format filename: image_pageXXXX.png.");
+                }
+                $pagenumber = (int)$matches[1];
+
+                if ($pagenumber == $index) {
+                    $source = imagecreatefromstring($file->get_content());
+                    $pagerotation = page_editor::get_page_rotation($grade->id, $index);
+                    $degree = empty($pagerotation) ? 0 : $pagerotation->degree;
+                    if ($rotateleft) {
+                        $content = imagerotate($source, 90, 0);
+                        $degree = ($degree + 90) % 360;
+                    } else {
+                        $content = imagerotate($source, -90, 0);
+                        $degree = ($degree - 90) % 360;
+                    }
+                    $filename = $matches[0].'png';
+                    $tmpdir = make_temp_directory(self::COMPONENT . '/' . self::PAGE_IMAGE_FILEAREA . '/'
+                        . self::hash($assignment, $userid, $attemptnumber));
+                    $tempfile = $tmpdir . '/' . time() . '_' . $filename;
+                    imagepng($content, $tempfile);
+
+                    $filearea = self::PAGE_IMAGE_FILEAREA;
+                    $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
+
+                    unlink($tempfile);
+                    rmdir($tmpdir);
+                    imagedestroy($source);
+                    imagedestroy($content);
+                    $file->delete();
+                    if (!empty($newfile)) {
+                        page_editor::set_page_rotation($grade->id, $pagenumber, true, $newfile->get_pathnamehash(), $degree);
+                    }
+                    return $newfile;
+                }
+            }
+        }
+        return null;
+    }
 }
index de9c770..ce86fee 100644 (file)
@@ -396,4 +396,46 @@ class page_editor {
         $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;
+    }
+
 }
index fc15749..7723389 100644 (file)
@@ -70,7 +70,8 @@ class pdf extends \FPDI {
     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
@@ -551,7 +552,7 @@ class pdf extends \FPDI {
             throw new \coding_exception('The specified image output folder is not a valid folder');
         }
 
-        $imagefile = $this->imagefolder.'/image_page' . $pageno . '.png';
+        $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE . $pageno . '.png';
         $generate = true;
         if (file_exists($imagefile)) {
             if (filemtime($imagefile) > filemtime($this->filename)) {
@@ -583,7 +584,7 @@ class pdf extends \FPDI {
             }
         }
 
-        return 'image_page'.$pageno.'.png';
+        return self::IMAGE_PAGE . $pageno . '.png';
     }
 
     /**
@@ -689,7 +690,7 @@ class pdf extends \FPDI {
         $pdf->set_image_folder($tmperrorimagefolder);
         $image = $pdf->get_image(0);
         $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
-        $newimg = 'image_page' . $pageno . '.png';
+        $newimg = self::IMAGE_PAGE . $pageno . '.png';
 
         copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg);
         return $newimg;
@@ -736,7 +737,7 @@ class pdf extends \FPDI {
         }
 
         $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
-        @unlink($testimagefolder.'/image_page0.png'); // Delete any previous test images.
+        unlink($testimagefolder . '/' . self::IMAGE_PAGE . '0.png'); // Delete any previous test images.
 
         $pdf = new pdf();
         $pdf->set_pdf($testfile);
@@ -761,10 +762,49 @@ class pdf extends \FPDI {
         require_once($CFG->libdir.'/filelib.php');
 
         $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
-        $testimage = $testimagefolder.'/image_page0.png';
+        $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);
+
+        if ($imageinfo["width"] > $imageinfo["height"]) {
+            if ($size['w'] < $size['h']) {
+                $temp = $size['w'];
+                $size['w'] = $size['h'];
+                $size['h'] = $temp;
+            }
+            $orientation = 'L';
+        } else if ($imageinfo["width"] < $imageinfo["height"]) {
+            if ($size['w'] > $size['h']) {
+                $temp = $size['w'];
+                $size['w'] = $size['h'];
+                $size['h'] = $temp;
+            }
+            $orientation = 'P';
+        } else {
+            $orientation = 'P';
+        }
+        $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['w'], $size['h'],
+            '', '', '', false, null, '', false, false, 0);
+    }
 }
 
index c3fb4da..3ad2fba 100644 (file)
@@ -174,6 +174,7 @@ class provider implements
         // 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);
         // Submission records in assignfeedback_editpdf_queue will be cleaned up in a scheduled task
     }
 }
index 7e1ddb5..61fecbf 100644 (file)
@@ -42,6 +42,8 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
     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',
@@ -161,6 +163,13 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
         $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) {
@@ -193,7 +202,7 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
             $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 = $toolbar1 . $toolbar2 . $toolbar3 . $toolbar4;
+            $toolbars = $rotationtools . $toolbar1 . $toolbar2 . $toolbar3 . $toolbar4;
             $toolbargroup = html_writer::div($toolbars, 'toolbar_group', array('role' => 'toolbar_group'));
         }
 
index 3180246..3115411 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/assign/feedback/editpdf/db" VERSION="20180925" COMMENT="XMLDB file for Moodle mod/assign/feedback/editpdf"
+<XMLDB PATH="mod/assign/feedback/editpdf/db" VERSION="20190107" COMMENT="XMLDB file for Moodle mod/assign/feedback/editpdf"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="submissionid-submissionattempt" TYPE="unique" FIELDS="submissionid, submissionattempt"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="assignfeedback_editpdf_rot" COMMENT="Stores rotation information of a page.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="gradeid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="pageno" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Page number"/>
+        <FIELD NAME="pathnamehash" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="File path hash of the rotated page"/>
+        <FIELD NAME="isrotated" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the page is rotated or not"/>
+        <FIELD NAME="degree" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Rotation degree"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="gradeid" TYPE="foreign" FIELDS="gradeid" REFTABLE="assign_grades" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="gradeid_pageno" UNIQUE="true" FIELDS="gradeid, pageno"/>
+      </INDEXES>
+    </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 2cd3cd9..ef219b6 100644 (file)
@@ -95,5 +95,33 @@ function xmldb_assignfeedback_editpdf_upgrade($oldversion) {
     // Automatically generated Moodle v3.6.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019010800) {
+        // Define table assignfeedback_editpdf_rot to be created.
+        $table = new xmldb_table('assignfeedback_editpdf_rot');
+
+        // Adding fields to table assignfeedback_editpdf_rot.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('gradeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('pageno', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('pathnamehash', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
+        $table->add_field('isrotated', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('degree', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+
+        // Adding keys to table assignfeedback_editpdf_rot.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
+        $table->add_key('gradeid', XMLDB_KEY_FOREIGN, ['gradeid'], 'assign_grades', ['id']);
+
+        // Adding indexes to table assignfeedback_editpdf_rot.
+        $table->add_index('gradeid_pageno', XMLDB_INDEX_UNIQUE, ['gradeid', 'pageno']);
+
+        // Conditionally launch create table for assignfeedback_editpdf_rot.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Editpdf savepoint reached.
+        upgrade_plugin_savepoint(true, 2019010800, 'assignfeedback', 'editpdf');
+    }
+
     return true;
 }
index c6f0186..94ec1b0 100644 (file)
@@ -107,3 +107,5 @@ $string['white'] = 'White';
 $string['yellow'] = 'Yellow';
 $string['draftchangessaved'] = 'Draft annotations saved';
 $string['preparesubmissionsforannotation'] = 'Prepare submissions for annotation';
+$string['rotateleft'] = 'Rotate 90 degrees to the left';
+$string['rotateright'] = 'Rotate 90 degrees to the right';
diff --git a/mod/assign/feedback/editpdf/pix/rotate_left.svg b/mod/assign/feedback/editpdf/pix/rotate_left.svg
new file mode 100644 (file)
index 0000000..7fae260
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="16"
+   height="16"
+   viewBox="0 0 16 16"
+   preserveAspectRatio="xMinYMid meet"
+   overflow="visible"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="rotate_right.svg"
+   inkscape:version="0.92.3 (unknown)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="640"
+     inkscape:window-height="480"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="8"
+     inkscape:cy="8"
+     inkscape:window-x="758"
+     inkscape:window-y="140"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4" />
+  <path
+     d="M 0,8.1 C 0,4.1 3,0.7 7,0.3 v 3 C 5,3.8 3,5.8 3,8.2 c 0,2.8 2.2,5 5,5 2.8,0 5,-2.2 5,-5 0,-1.1 0,-2.1 -1,-3 v 1 C 12,6.7 11.5,7 10.9,7 h -1 C 9.4,7 9,6.7 9,6.2 v -5 C 9,0.6 9.4,0 9.9,0 h 5 C 15.5,0 16,0.6 16,1.2 v 1 C 16,2.7 15.5,3 14.9,3 H 14 c 1.1,1 1.8,3.1 1.8,5 0,4.4 -3.5,8 -7.9,8 C 3.6,16 0,12.5 0,8.1 Z"
+     id="path2"
+     inkscape:connector-curvature="0"
+     style="fill:#999999" />
+</svg>
diff --git a/mod/assign/feedback/editpdf/pix/rotate_right.svg b/mod/assign/feedback/editpdf/pix/rotate_right.svg
new file mode 100644 (file)
index 0000000..7bbde0e
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="16"
+   height="16"
+   viewBox="0 0 16 16"
+   preserveAspectRatio="xMinYMid meet"
+   overflow="visible"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="rotate_left.svg"
+   inkscape:version="0.92.3 (unknown)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="640"
+     inkscape:window-height="480"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="8"
+     inkscape:cy="8"
+     inkscape:window-x="758"
+     inkscape:window-y="140"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4" />
+  <path
+     d="M 16,8.1 C 16,4.1 13,0.7 9,0.3 v 3 c 2,0.5 4,2.5 4,4.9 0,2.8 -2.2,5 -5,5 -2.8,0 -5,-2.2 -5,-5 0,-1.1 0,-2.1 1,-3 v 1 C 4,6.7 4.5,7 5.1,7 h 1 C 6.6,7 7,6.7 7,6.2 v -5 C 7,0.6 6.6,0 6.1,0 h -5 C 0.5,0 0,0.6 0,1.2 v 1 C 0,2.7 0.5,3 1.1,3 H 2 C 0.9,4 0.2,6.1 0.2,8 c 0,4.4 3.5,8 7.9,8 4.3,0 7.9,-3.5 7.9,-7.9 z"
+     id="path2"
+     inkscape:connector-curvature="0"
+     style="fill:#999999" />
+</svg>
index feade3f..e8e0a59 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;
+$plugin->version   = 2019010800;
 $plugin->requires  = 2018112800;
 $plugin->component = 'assignfeedback_editpdf';
index 051eb5d..b92fa9f 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 26e4b5d..9132cfd 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 051eb5d..b92fa9f 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 2451176..5fa232c 100644 (file)
@@ -618,6 +618,23 @@ var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
         return (bounds.has_min_width() && bounds.has_min_height());
     };
 
+    /**
+     * Update comment position when rotating page.
+     * @public
+     * @method updatePosition
+     */
+    this.updatePosition = function() {
+        var node = this.drawable.nodes[0].one('textarea');
+        var container = node.ancestor('div');
+
+        var newlocation = new M.assignfeedback_editpdf.point(this.x, this.y);
+        var windowlocation = this.editor.get_window_coordinates(newlocation);
+
+        container.setX(windowlocation.x);
+        container.setY(windowlocation.y);
+        this.drawable.store_position(container, windowlocation.x, windowlocation.y);
+    };
+
 };
 
 M.assignfeedback_editpdf = M.assignfeedback_editpdf || {};
index 4e48970..579c33f 100644 (file)
@@ -35,6 +35,11 @@ var EDITOR = function() {
 };
 EDITOR.prototype = {
 
+    /**
+     * Store old coordinates of the annotations before rotation happens.
+     */
+    oldannotationcoordinates: null,
+
     /**
      * The dialogue used for all action menu displays.
      *
@@ -797,6 +802,8 @@ EDITOR.prototype = {
             annotationcolourbutton,
             searchcommentsbutton,
             expcolcommentsbutton,
+            rotateleftbutton,
+            rotaterightbutton,
             currentstampbutton,
             stampfiles,
             picker,
@@ -813,6 +820,17 @@ EDITOR.prototype = {
         if (this.get('readonly')) {
             return;
         }
+
+        // Rotate Left.
+        rotateleftbutton = this.get_dialogue_element(SELECTOR.ROTATELEFTBUTTON);
+        rotateleftbutton.on('click', this.rotatePDF, this, true);
+        rotateleftbutton.on('key', this.rotatePDF, 'down:13', this, true);
+
+        // Rotate Right.
+        rotaterightbutton = this.get_dialogue_element(SELECTOR.ROTATERIGHTBUTTON);
+        rotaterightbutton.on('click', this.rotatePDF, this, false);
+        rotaterightbutton.on('key', this.rotatePDF, 'down:13', this, false);
+
         this.disable_touch_scroll();
 
         // Setup the tool buttons.
@@ -1468,6 +1486,103 @@ EDITOR.prototype = {
         }
     },
 
+    /**
+     * Calculate degree to rotate.
+     * @protected
+     * @param {Object} e javascript event
+     * @param {boolean} left  true if rotating left, false if rotating right
+     * @method rotatepdf
+     */
+    rotatePDF: function(e, left) {
+        e.preventDefault();
+
+        if (this.get('destroyed')) {
+            return;
+        }
+        var self = this;
+        // Save old coordinates.
+        var i;
+        this.oldannotationcoordinates = [];
+        var annotations = this.pages[this.currentpage].annotations;
+        for (i = 0; i < annotations.length; i++) {
+            var oldannotation = annotations[i];
+            this.oldannotationcoordinates.push([oldannotation.x, oldannotation.y]);
+        }
+
+        var ajaxurl = AJAXBASE;
+        var config = {
+            method: 'post',
+            context: this,
+            sync: false,
+            data: {
+                'sesskey': M.cfg.sesskey,
+                'action': 'rotatepage',
+                'index': this.currentpage,
+                'userid': this.get('userid'),
+                'attemptnumber': this.get('attemptnumber'),
+                'assignmentid': this.get('assignmentid'),
+                'rotateleft': left
+            },
+            on: {
+                success: function(tid, response) {
+                    var jsondata;
+                    try {
+                        jsondata = Y.JSON.parse(response.responseText);
+                        var page = self.pages[self.currentpage];
+                        page.url = jsondata.page.url;
+                        page.width = jsondata.page.width;
+                        page.height = jsondata.page.height;
+                        self.loadingicon.hide();
+
+                        // Change canvas size to fix the new page.
+                        var drawingcanvas = self.get_dialogue_element(SELECTOR.DRAWINGCANVAS);
+                        drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
+                        drawingcanvas.setStyle('width', page.width + 'px');
+                        drawingcanvas.setStyle('height', page.height + 'px');
+
+                        /**
+                         * Move annotation to old position.
+                         * Reason: When canvas size change
+                         * > Shape annotations move with relation to canvas coordinates
+                         * > Nodes of stamp annotations move with relation to canvas coordinates
+                         * > Presentation (picture) of stamp annotations  stay to document coordinates (stick to its own position)
+                         * > Without relocating the node and presentation of a stamp annotation to the same x,y position,
+                         * the stamp annotation cannot be chosen when using "drag" tool.
+                         * The following code brings all annotations to their old positions with relation to the canvas coordinates.
+                         */
+                        var i;
+                        // Annotations.
+                        var annotations = page.annotations;
+                        for (i = 0; i < annotations.length; i++) {
+                            if (self.oldannotationcoordinates && self.oldannotationcoordinates[i]) {
+                                var oldX = self.oldannotationcoordinates[i][0];
+                                var oldY = self.oldannotationcoordinates[i][1];
+                                var annotation = annotations[i];
+                                annotation.move(oldX, oldY);
+                            }
+                        }
+                        /**
+                         * Update Position of comments with relation to canvas coordinates.
+                         * Without this code, the comments will stay at their positions in windows/document coordinates.
+                         */
+                        var oldcomments = page.comments;
+                        for (i = 0; i < oldcomments.length; i++) {
+                            oldcomments[i].updatePosition();
+                        }
+                        // Save Annotations.
+                        return self.save_current_page();
+                    } catch (e) {
+                        return new M.core.exception(e);
+                    }
+                },
+                failure: function(tid, response) {
+                    return new M.core.exception(response.responseText);
+                }
+            }
+        };
+        Y.io(ajaxurl, config);
+    },
+
     /**
      * Test the browser support for options objects on event listeners.
      * @return Boolean
index ae9a0ce..7fdb6cc 100644 (file)
@@ -47,6 +47,8 @@ var AJAXBASE = M.cfg.wwwroot + '/mod/assign/feedback/editpdf/ajax.php',
         UNSAVEDCHANGESINPUT: 'input[name="assignfeedback_editpdf_haschanges"]',
         STAMPSBUTTON: '.currentstampbutton',
         USERINFOREGION: '[data-region="user-info"]',
+        ROTATELEFTBUTTON: '.rotateleftbutton',
+        ROTATERIGHTBUTTON: '.rotaterightbutton',
         DIALOGUE: '.' + CSS.DIALOGUE
     },
     SELECTEDBORDERCOLOUR = 'rgba(200, 200, 255, 0.9)',