MDL-63349 assignfeedback_editpdf: Rotate submitted image automatically
authorNathan Nguyen <nathannguyen@catalyst-au.net>
Tue, 26 Mar 2019 23:21:05 +0000 (10:21 +1100)
committerNathan Nguyen <nathannguyen@catalyst-au.net>
Fri, 11 Oct 2019 04:26:53 +0000 (15:26 +1100)
lib/filestorage/stored_file.php
lib/upgrade.txt
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/submission/file/locallib.php
mod/assign/submissionplugin.php
mod/assign/upgrade.txt

index f1c3f75..fc01b1e 100644 (file)
@@ -1130,4 +1130,47 @@ class stored_file {
     public function compare_to_string($content) {
         return $this->get_contenthash() === file_storage::hash_from_string($content);
     }
+
+    /**
+     * Generate a rotated image for this stored_file based on exif information.
+     *
+     * @return array|false False when a problem occurs, else the image data and image size.
+     * @since Moodle 3.8
+     */
+    public function rotate_image() {
+        $content = $this->get_content();
+        $mimetype = $this->get_mimetype();
+
+        if ($mimetype === "image/jpeg" && function_exists("exif_read_data")) {
+            $exif = @exif_read_data("data://image/jpeg;base64," . base64_encode($content));
+            if (isset($exif['ExifImageWidth']) && isset($exif['ExifImageLength']) && isset($exif['Orientation'])) {
+                $rotation = [
+                    3 => -180,
+                    6 => -90,
+                    8 => -270,
+                ];
+                $orientation = $exif['Orientation'];
+                if ($orientation !== 1) {
+                    $source = @imagecreatefromstring($content);
+                    $data = @imagerotate($source, $rotation[$orientation], 0);
+                    if (!empty($data)) {
+                        if ($orientation == 1 || $orientation == 3) {
+                            $size = [
+                                'width' => $exif["ExifImageWidth"],
+                                'height' => $exif["ExifImageLength"],
+                            ];
+                        } else {
+                            $size = [
+                                'height' => $exif["ExifImageWidth"],
+                                'width' => $exif["ExifImageLength"],
+                            ];
+                        }
+                        imagedestroy($source);
+                        return [$data, $size];
+                    }
+                }
+            }
+        }
+        return [false, false];
+    }
 }
index 88d411a..c7d4957 100644 (file)
@@ -2,7 +2,7 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.8 ===
-
+* The rotate_image function has been added to the stored_file class (MDL-63349)
 * The yui checknet module is removed. Call \core\session\manager::keepalive instead.
 * The generate_uuid() function has been deprecated. Please use \core\uuid::generate() instead.
 * Remove lib/pear/auth/RADIUS.php (MDL-65746)
index 6060236..9c02a10 100644 (file)
@@ -56,6 +56,10 @@ class document_services {
     const STAMPS_FILEAREA = 'stamps';
     /** Filename for combined pdf */
     const COMBINED_PDF_FILENAME = 'combined.pdf';
+    /**  Temporary place to save JPG Image to PDF file */
+    const TMP_JPG_TO_PDF_FILEAREA = 'tmp_jpg_to_pdf';
+    /**  Temporary place to save (Automatically) Rotated JPG FILE */
+    const TMP_ROTATED_JPG_FILEAREA = 'tmp_rotated_jpg';
     /** Hash of blank pdf */
     const BLANK_PDF_HASH = '4c803c92c71f21b423d13de570c8a09e0a31c718';
 
@@ -187,9 +191,28 @@ EOD;
                 $pluginfiles = $plugin->get_files($submission, $user);
                 foreach ($pluginfiles as $filename => $file) {
                     if ($file instanceof \stored_file) {
-                        if ($file->get_mimetype() === 'application/pdf') {
+                        $mimetype = $file->get_mimetype();
+                        // PDF File, no conversion required.
+                        if ($mimetype === 'application/pdf') {
                             $files[$filename] = $file;
-                        } else if ($convertedfile = $converter->start_conversion($file, 'pdf')) {
+                        } else if ($plugin->allow_image_conversion() && $mimetype === "image/jpeg") {
+                            // Rotates image based on the EXIF value.
+                            list ($rotateddata, $size) = $file->rotate_image();
+                            if ($rotateddata) {
+                                $file = self::save_rotated_image_file($assignment, $userid, $attemptnumber,
+                                    $rotateddata, $filename);
+                            }
+                            // Save as PDF file if there is no available converter.
+                            if (!$converter->can_convert_format_to('jpg', 'pdf')) {
+                                $pdffile = self::save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size);
+                                if ($pdffile) {
+                                    $files[$filename] = $pdffile;
+                                }
+                            }
+                        }
+                        // The file has not been converted to PDF, try to convert it to PDF.
+                        if (!isset($files[$filename])
+                            && $convertedfile = $converter->start_conversion($file, 'pdf')) {
                             $files[$filename] = $convertedfile;
                         }
                     } else if ($converter->can_convert_format_to('html', 'pdf')) {
@@ -967,4 +990,83 @@ EOD;
         }
         return null;
     }
+
+    /**
+     * Convert jpg file to pdf file
+     * @param int|\assign $assignment Assignment
+     * @param int $userid User ID
+     * @param int $attemptnumber Attempt Number
+     * @param \stored_file $file file to save
+     * @param null|array $size size of image
+     * @return \stored_file
+     * @throws \file_exception
+     * @throws \stored_file_creation_exception
+     */
+    private static function save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size=null) {
+        // Temporary file.
+        $filename = $file->get_filename();
+        $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
+            . self::TMP_JPG_TO_PDF_FILEAREA . DIRECTORY_SEPARATOR
+            . self::hash($assignment, $userid, $attemptnumber));
+        $tempfile = $tmpdir . DIRECTORY_SEPARATOR . $filename . ".pdf";
+        // Determine orientation.
+        $orientation = 'P';
+        if (!empty($size['width']) && !empty($size['height'])) {
+            if ($size['width'] > $size['height']) {
+                $orientation = 'L';
+            }
+        }
+        // Save JPG image to PDF file.
+        $pdf = new pdf();
+        $pdf->SetHeaderMargin(0);
+        $pdf->SetFooterMargin(0);
+        $pdf->SetMargins(0, 0, 0, true);
+        $pdf->setPrintFooter(false);
+        $pdf->setPrintHeader(false);
+        $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
+        $pdf->AddPage($orientation);
+        $pdf->SetAutoPageBreak(false);
+        // Width has to be define here to fit into A4 page. Otherwise the image will be inserted with original size.
+        if ($orientation == 'P') {
+            $pdf->Image('@' . $file->get_content(), 0, 0, 210);
+        } else {
+            $pdf->Image('@' . $file->get_content(), 0, 0, 297);
+        }
+        $pdf->setPageMark();
+        $pdf->save_pdf($tempfile);
+        $filearea = self::TMP_JPG_TO_PDF_FILEAREA;
+        $pdffile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
+        if (file_exists($tempfile)) {
+            unlink($tempfile);
+            rmdir($tmpdir);
+        }
+        return $pdffile;
+    }
+
+    /**
+     * Save rotated image data to file.
+     * @param int|\assign $assignment Assignment
+     * @param int $userid User ID
+     * @param int $attemptnumber Attempt Number
+     * @param resource $rotateddata image data to save
+     * @param string $filename name of the image file
+     * @return \stored_file
+     * @throws \file_exception
+     * @throws \stored_file_creation_exception
+     */
+    private static function save_rotated_image_file($assignment, $userid, $attemptnumber, $rotateddata, $filename) {
+        $filearea = self::TMP_ROTATED_JPG_FILEAREA;
+        $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
+            . $filearea . DIRECTORY_SEPARATOR
+            . self::hash($assignment, $userid, $attemptnumber));
+        $tempfile = $tmpdir . DIRECTORY_SEPARATOR . basename($filename);
+        imagejpeg($rotateddata, $tempfile);
+        $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
+        if (file_exists($tempfile)) {
+            unlink($tempfile);
+            rmdir($tmpdir);
+        }
+        return $newfile;
+    }
+
 }
index 7ad838a..e0e0539 100644 (file)
@@ -637,4 +637,12 @@ class assign_submission_file extends assign_submission_plugin {
 
         return $sets;
     }
+
+    /**
+     * Determine if the plugin allows image file conversion
+     * @return bool
+     */
+    public function allow_image_conversion() {
+        return true;
+    }
 }
index 8bbcb9a..2280cd0 100644 (file)
@@ -146,4 +146,12 @@ abstract class assign_submission_plugin extends assign_plugin {
     public function submission_is_empty(stdClass $data) {
         return false;
     }
+
+    /**
+     * Determine if the plugin allows image file conversion
+     * @return bool
+     */
+    public function allow_image_conversion() {
+        return false;
+    }
 }
index 5268533..e68d31c 100644 (file)
@@ -1,5 +1,7 @@
 This files describes API changes in the assign code.
 === 3.8 ===
+* The allow_image_conversion method has been added to the submissionplugins. It determines whether the submission plugin
+  allows image conversion or not. By default conversion is not allowed (except when overwritten in the submission plugin)
 * Webservice function mod_assign_get_submission_status, return value 'warnofungroupedusers', changed from PARAM_BOOL to PARAM_ALPHA. See the description for possible values.
 * The following functions have been finally deprecated and can not be used anymore:
     * assign_scale_used()