Differences Between: [Versions 310 and 402] [Versions 310 and 403]
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 combined document class for the assignfeedback_editpdf plugin. 19 * 20 * @package assignfeedback_editpdf 21 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace assignfeedback_editpdf; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * The combined_document class for the assignfeedback_editpdf plugin. 31 * 32 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class combined_document { 36 37 /** 38 * Status value representing a conversion waiting to start. 39 */ 40 const STATUS_PENDING_INPUT = 0; 41 42 /** 43 * Status value representing all documents ready to be combined. 44 */ 45 const STATUS_READY = 1; 46 47 /** 48 * Status value representing all documents are ready to be combined as are supported. 49 */ 50 const STATUS_READY_PARTIAL = 3; 51 52 /** 53 * Status value representing a successful conversion. 54 */ 55 const STATUS_COMPLETE = 2; 56 57 /** 58 * Status value representing a permanent error. 59 */ 60 const STATUS_FAILED = -1; 61 62 /** 63 * The list of files which make this document. 64 */ 65 protected $sourcefiles = []; 66 67 /** 68 * The resultant combined file. 69 */ 70 protected $combinedfile; 71 72 /** 73 * The combination status. 74 */ 75 protected $combinationstatus = null; 76 77 /** 78 * The number of pages in the combined PDF. 79 */ 80 protected $pagecount = 0; 81 82 /** 83 * Check the current status of the document combination. 84 * Note that the combined document may not contain all the source files if some of the 85 * source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only 86 * the cover sheet will be included in the combined document. 87 * 88 * @return int 89 */ 90 public function get_status() { 91 if ($this->combinedfile) { 92 // The combined file exists. Report success. 93 return self::STATUS_COMPLETE; 94 } 95 96 if (empty($this->sourcefiles)) { 97 // There are no source files to combine. 98 return self::STATUS_FAILED; 99 } 100 101 if (!empty($this->combinationstatus)) { 102 // The combination is in progress and has set a status. 103 // Return it instead. 104 return $this->combinationstatus; 105 } 106 107 $pending = false; 108 $partial = false; 109 foreach ($this->sourcefiles as $file) { 110 // The combined file has not yet been generated. 111 // Check the status of each source file. 112 if (is_a($file, \core_files\conversion::class)) { 113 $status = $file->get('status'); 114 switch ($status) { 115 case \core_files\conversion::STATUS_IN_PROGRESS: 116 case \core_files\conversion::STATUS_PENDING: 117 $pending = true; 118 break; 119 120 // There are 4 status flags, so the only remaining one is complete which is fine. 121 case \core_files\conversion::STATUS_FAILED: 122 $partial = true; 123 break; 124 } 125 } 126 } 127 if ($pending) { 128 return self::STATUS_PENDING_INPUT; 129 } else { 130 if ($partial) { 131 return self::STATUS_READY_PARTIAL; 132 } 133 return self::STATUS_READY; 134 } 135 } 136 /** 137 * Set the completed combined file. 138 * 139 * @param stored_file $file The completed document for all files to be combined. 140 * @return $this 141 */ 142 public function set_combined_file($file) { 143 $this->combinedfile = $file; 144 145 return $this; 146 } 147 148 /** 149 * Return true of the combined file contained only some of the submission files. 150 * 151 * @return boolean 152 */ 153 public function is_partial_conversion() { 154 $combinedfile = $this->get_combined_file(); 155 if (empty($combinedfile)) { 156 return false; 157 } 158 $filearea = $combinedfile->get_filearea(); 159 return $filearea == document_services::PARTIAL_PDF_FILEAREA; 160 } 161 162 /** 163 * Retrieve the completed combined file. 164 * 165 * @return stored_file 166 */ 167 public function get_combined_file() { 168 return $this->combinedfile; 169 } 170 171 /** 172 * Set all source files which are to be combined. 173 * 174 * @param stored_file|conversion[] $files The complete list of all source files to be combined. 175 * @return $this 176 */ 177 public function set_source_files($files) { 178 $this->sourcefiles = $files; 179 180 return $this; 181 } 182 183 /** 184 * Add an additional source file to the end of the existing list. 185 * 186 * @param stored_file|conversion $file The file to add to the end of the list. 187 * @return $this 188 */ 189 public function add_source_file($file) { 190 $this->sourcefiles[] = $file; 191 192 return $this; 193 } 194 195 /** 196 * Retrieve the complete list of source files. 197 * 198 * @return stored_file|conversion[] 199 */ 200 public function get_source_files() { 201 return $this->sourcefiles; 202 } 203 204 /** 205 * Refresh the files. 206 * 207 * This includes polling any pending conversions to see if they are complete. 208 * 209 * @return $this 210 */ 211 public function refresh_files() { 212 $converter = new \core_files\converter(); 213 foreach ($this->sourcefiles as $file) { 214 if (is_a($file, \core_files\conversion::class)) { 215 $status = $file->get('status'); 216 switch ($status) { 217 case \core_files\conversion::STATUS_COMPLETE: 218 continue 2; 219 break; 220 default: 221 $converter->poll_conversion($conversion); 222 } 223 } 224 } 225 226 return $this; 227 } 228 229 /** 230 * Combine all source files into a single PDF and store it in the 231 * file_storage API using the supplied contextid and itemid. 232 * 233 * @param int $contextid The contextid for the file to be stored under 234 * @param int $itemid The itemid for the file to be stored under 235 * @return $this 236 */ 237 public function combine_files($contextid, $itemid) { 238 global $CFG; 239 240 $currentstatus = $this->get_status(); 241 $readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL]; 242 if ($currentstatus === self::STATUS_FAILED) { 243 $this->store_empty_document($contextid, $itemid); 244 245 return $this; 246 } else if (!in_array($currentstatus, $readystatuslist)) { 247 // The document is either: 248 // * already combined; or 249 // * pending input being fully converted; or 250 // * unable to continue due to an issue with the input documents. 251 // 252 // Exit early as we cannot continue. 253 return $this; 254 } 255 256 require_once($CFG->libdir . '/pdflib.php'); 257 258 $pdf = new pdf(); 259 $files = $this->get_source_files(); 260 $compatiblepdfs = []; 261 262 foreach ($files as $file) { 263 // Check that each file is compatible and add it to the list. 264 // Note: We drop non-compatible files. 265 $compatiblepdf = false; 266 if (is_a($file, \core_files\conversion::class)) { 267 $status = $file->get('status'); 268 if ($status == \core_files\conversion::STATUS_COMPLETE) { 269 $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile()); 270 } 271 } else { 272 $compatiblepdf = pdf::ensure_pdf_compatible($file); 273 } 274 275 if ($compatiblepdf) { 276 $compatiblepdfs[] = $compatiblepdf; 277 } 278 } 279 280 $tmpdir = make_request_directory(); 281 $tmpfile = $tmpdir . '/' . document_services::COMBINED_PDF_FILENAME; 282 283 try { 284 $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile); 285 $pdf->Close(); 286 } catch (\Exception $e) { 287 // Unable to combine the PDF. 288 debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER); 289 290 $pdf->Close(); 291 return $this->mark_combination_failed(); 292 } 293 294 // Verify the PDF. 295 $verifypdf = new pdf(); 296 $verifypagecount = $verifypdf->load_pdf($tmpfile); 297 $verifypdf->Close(); 298 299 if ($verifypagecount <= 0) { 300 // No pages were found in the combined PDF. 301 return $this->mark_combination_failed(); 302 } 303 304 // Store the newly created file as a stored_file. 305 $this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL)); 306 307 // Note the verified page count. 308 $this->pagecount = $verifypagecount; 309 310 return $this; 311 } 312 313 /** 314 * Mark the combination attempt as having encountered a permanent failure. 315 * 316 * @return $this 317 */ 318 protected function mark_combination_failed() { 319 $this->combinationstatus = self::STATUS_FAILED; 320 321 return $this; 322 } 323 324 /** 325 * Store the combined file in the file_storage API. 326 * 327 * @param string $tmpfile The path to the file on disk to be stored. 328 * @param int $contextid The contextid for the file to be stored under 329 * @param int $itemid The itemid for the file to be stored under 330 * @param boolean $partial The combined pdf contains only some of the source files. 331 * @return $this 332 */ 333 protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) { 334 // Store the file. 335 $record = $this->get_stored_file_record($contextid, $itemid, $partial); 336 $fs = get_file_storage(); 337 338 // Delete existing files first. 339 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); 340 341 // This was a combined pdf. 342 $file = $fs->create_file_from_pathname($record, $tmpfile); 343 344 $this->set_combined_file($file); 345 346 return $this; 347 } 348 349 /** 350 * Store the empty document file in the file_storage API. 351 * 352 * @param int $contextid The contextid for the file to be stored under 353 * @param int $itemid The itemid for the file to be stored under 354 * @return $this 355 */ 356 protected function store_empty_document($contextid, $itemid) { 357 // Store the file. 358 $record = $this->get_stored_file_record($contextid, $itemid); 359 $fs = get_file_storage(); 360 361 // Delete existing files first. 362 $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); 363 364 $file = $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64)); 365 $this->pagecount = 1; 366 367 $this->set_combined_file($file); 368 369 return $this; 370 } 371 372 /** 373 * Get the total number of pages in the combined document. 374 * 375 * If there are no pages, or it is not yet possible to count them a 376 * value of 0 is returned. 377 * 378 * @return int 379 */ 380 public function get_page_count() { 381 if ($this->pagecount) { 382 return $this->pagecount; 383 } 384 385 $status = $this->get_status(); 386 387 if ($status === self::STATUS_FAILED) { 388 // The empty document will be returned. 389 return 1; 390 } 391 392 if ($status !== self::STATUS_COMPLETE) { 393 // No pages yet. 394 return 0; 395 } 396 397 // Load the PDF to determine the page count. 398 $temparea = make_request_directory(); 399 $tempsrc = $temparea . "/source.pdf"; 400 $this->get_combined_file()->copy_content_to($tempsrc); 401 402 $pdf = new pdf(); 403 $pagecount = $pdf->load_pdf($tempsrc); 404 $pdf->Close(); 405 406 if ($pagecount <= 0) { 407 // Something went wrong. Return an empty page count again. 408 return 0; 409 } 410 411 $this->pagecount = $pagecount; 412 return $this->pagecount; 413 } 414 415 /** 416 * Get the total number of documents to be combined. 417 * 418 * @return int 419 */ 420 public function get_document_count() { 421 return count($this->sourcefiles); 422 } 423 424 /** 425 * Helper to fetch the stored_file record. 426 * 427 * @param int $contextid The contextid for the file to be stored under 428 * @param int $itemid The itemid for the file to be stored under 429 * @param boolean $partial The combined file contains only some of the source files. 430 * @return stdClass 431 */ 432 protected function get_stored_file_record($contextid, $itemid, $partial = false) { 433 $filearea = document_services::COMBINED_PDF_FILEAREA; 434 if ($partial) { 435 $filearea = document_services::PARTIAL_PDF_FILEAREA; 436 } 437 return (object) [ 438 'contextid' => $contextid, 439 'component' => 'assignfeedback_editpdf', 440 'filearea' => $filearea, 441 'itemid' => $itemid, 442 'filepath' => '/', 443 'filename' => document_services::COMBINED_PDF_FILENAME, 444 ]; 445 } 446 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body