From 4a6edc5767b7b40968513ed6a060917ba1921435 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Wed, 27 Mar 2019 10:21:05 +1100 Subject: [PATCH] MDL-63349 assignfeedback_editpdf: Rotate submitted image automatically --- lib/filestorage/stored_file.php | 43 +++++++ lib/upgrade.txt | 2 +- .../editpdf/classes/document_services.php | 106 +++++++++++++++++- mod/assign/submission/file/locallib.php | 8 ++ mod/assign/submissionplugin.php | 8 ++ mod/assign/upgrade.txt | 2 + 6 files changed, 166 insertions(+), 3 deletions(-) diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index f1c3f759f03..fc01b1ecae7 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -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]; + } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 88d411a13af..c7d495706c7 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -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) diff --git a/mod/assign/feedback/editpdf/classes/document_services.php b/mod/assign/feedback/editpdf/classes/document_services.php index 6060236d0dd..9c02a10cb51 100644 --- a/mod/assign/feedback/editpdf/classes/document_services.php +++ b/mod/assign/feedback/editpdf/classes/document_services.php @@ -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; + } + } diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php index 7ad838afd3c..e0e0539e1bd 100644 --- a/mod/assign/submission/file/locallib.php +++ b/mod/assign/submission/file/locallib.php @@ -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; + } } diff --git a/mod/assign/submissionplugin.php b/mod/assign/submissionplugin.php index 8bbcb9a48fd..2280cd094cf 100644 --- a/mod/assign/submissionplugin.php +++ b/mod/assign/submissionplugin.php @@ -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; + } } diff --git a/mod/assign/upgrade.txt b/mod/assign/upgrade.txt index 52685333bbe..e68d31c5ce3 100644 --- a/mod/assign/upgrade.txt +++ b/mod/assign/upgrade.txt @@ -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()