PageRenderTime 75ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/mod/assign/feedback/editpdf/classes/combined_document.php

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