Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}