Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
 * Library code for manipulating PDFs
19
 *
20
 * @package assignfeedback_editpdf
21
 * @copyright 2012 Davo Smith
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace assignfeedback_editpdf;
26
 
1441 ariadna 27
use setasign\Fpdi\Tcpdf\Fpdi;
28
 
1 efrain 29
defined('MOODLE_INTERNAL') || die();
30
 
31
global $CFG;
32
require_once($CFG->libdir.'/pdflib.php');
1441 ariadna 33
require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/src/autoload.php');
1 efrain 34
 
35
/**
36
 * Library code for manipulating PDFs
37
 *
38
 * @package assignfeedback_editpdf
39
 * @copyright 2012 Davo Smith
40
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 */
1441 ariadna 42
class pdf extends Fpdi {
1 efrain 43
 
44
    /** @var int the number of the current page in the PDF being processed */
45
    protected $currentpage = 0;
46
    /** @var int the total number of pages in the PDF being processed */
47
    protected $pagecount = 0;
48
    /** @var float used to scale the pixel position of annotations (in the database) to the position in the final PDF */
49
    protected $scale = 0.0;
50
    /** @var string the path in which to store generated page images */
51
    protected $imagefolder = null;
52
    /** @var string the path to the PDF currently being processed */
53
    protected $filename = null;
54
    /** @var string the fontname used when the PDF being processed */
55
    protected $fontname = null;
56
 
57
    /** No errors */
58
    const GSPATH_OK = 'ok';
59
    /** Not set */
60
    const GSPATH_EMPTY = 'empty';
61
    /** Does not exist */
62
    const GSPATH_DOESNOTEXIST = 'doesnotexist';
63
    /** Is a dir */
64
    const GSPATH_ISDIR = 'isdir';
65
    /** Not executable */
66
    const GSPATH_NOTEXECUTABLE = 'notexecutable';
67
    /** Test file missing */
68
    const GSPATH_NOTESTFILE = 'notestfile';
69
    /** Any other error */
70
    const GSPATH_ERROR = 'error';
71
    /** Min. width an annotation should have */
72
    const MIN_ANNOTATION_WIDTH = 5;
73
    /** Min. height an annotation should have */
74
    const MIN_ANNOTATION_HEIGHT = 5;
75
    /** Blank PDF file used during error. */
76
    const BLANK_PDF = '/mod/assign/feedback/editpdf/fixtures/blank.pdf';
77
    /** Page image file name prefix*/
78
    const IMAGE_PAGE = 'image_page';
79
    /**
80
     * Get the name of the font to use in generated PDF files.
81
     * If $CFG->pdfexportfont is set - use it, otherwise use "freesans" as this
82
     * open licensed font has wide support for different language charsets.
83
     *
84
     * @return string
85
     */
86
    private function get_export_font_name() {
87
        $fontname = 'freesans';
88
        if (!empty($this->fontname)) {
89
            $fontname = $this->fontname;
90
        }
91
        return $fontname;
92
    }
93
 
94
    /**
95
     * Set font name.
96
     *
97
     * @param string $fontname Font name which is
98
     * @return void
99
     */
100
    public function set_export_font_name($fontname): void {
101
        $this->fontname = $fontname;
102
    }
103
 
104
    /**
105
     * Combine the given PDF files into a single PDF. Optionally add a coversheet and coversheet fields.
106
     * @param string[] $pdflist  the filenames of the files to combine
107
     * @param string $outfilename the filename to write to
108
     * @return int the number of pages in the combined PDF
109
     */
110
    public function combine_pdfs($pdflist, $outfilename) {
111
 
112
        raise_memory_limit(MEMORY_EXTRA);
113
        $olddebug = error_reporting(0);
114
 
115
        $this->setPageUnit('pt');
116
        $this->setPrintHeader(false);
117
        $this->setPrintFooter(false);
118
        $this->scale = 72.0 / 100.0;
119
        // Use font supporting the widest range of characters.
120
        $this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true);
121
        $this->SetTextColor(0, 0, 0);
122
 
123
        $totalpagecount = 0;
124
 
125
        foreach ($pdflist as $file) {
126
            $pagecount = $this->setSourceFile($file);
127
            $totalpagecount += $pagecount;
128
            for ($i = 1; $i<=$pagecount; $i++) {
129
                $this->create_page_from_source($i);
130
            }
131
        }
132
 
133
        $this->save_pdf($outfilename);
134
        error_reporting($olddebug);
135
 
136
        return $totalpagecount;
137
    }
138
 
139
    /**
140
     * The number of the current page in the PDF being processed
141
     * @return int
142
     */
143
    public function current_page() {
144
        return $this->currentpage;
145
    }
146
 
147
    /**
148
     * The total number of pages in the PDF being processed
149
     * @return int
150
     */
151
    public function page_count() {
152
        return $this->pagecount;
153
    }
154
 
155
    /**
156
     * Load the specified PDF and set the initial output configuration
157
     * Used when processing comments and outputting a new PDF
158
     * @param string $filename the path to the PDF to load
159
     * @return int the number of pages in the PDF
160
     */
161
    public function load_pdf($filename) {
162
        raise_memory_limit(MEMORY_EXTRA);
163
        $olddebug = error_reporting(0);
164
 
165
        $this->setPageUnit('pt');
166
        $this->scale = 72.0 / 100.0;
167
        $this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true);
168
        $this->SetFillColor(255, 255, 176);
169
        $this->SetDrawColor(0, 0, 0);
170
        $this->SetLineWidth(1.0 * $this->scale);
171
        $this->SetTextColor(0, 0, 0);
172
        $this->setPrintHeader(false);
173
        $this->setPrintFooter(false);
174
        $this->pagecount = $this->setSourceFile($filename);
175
        $this->filename = $filename;
176
 
177
        error_reporting($olddebug);
178
        return $this->pagecount;
179
    }
180
 
181
    /**
182
     * Sets the name of the PDF to process, but only loads the file if the
183
     * pagecount is zero (in order to count the number of pages)
184
     * Used when generating page images (but not a new PDF)
185
     * @param string $filename the path to the PDF to process
186
     * @param int $pagecount optional the number of pages in the PDF, if known
187
     * @return int the number of pages in the PDF
188
     */
189
    public function set_pdf($filename, $pagecount = 0) {
190
        if ($pagecount == 0) {
191
            return $this->load_pdf($filename);
192
        } else {
193
            $this->filename = $filename;
194
            $this->pagecount = $pagecount;
195
            return $pagecount;
196
        }
197
    }
198
 
199
    /**
200
     * Copy the next page from the source file and set it as the current page
201
     * @return bool true if successful
202
     */
203
    public function copy_page() {
204
        if (!$this->filename) {
205
            return false;
206
        }
207
        if ($this->currentpage>=$this->pagecount) {
208
            return false;
209
        }
210
        $this->currentpage++;
211
        $this->create_page_from_source($this->currentpage);
212
        return true;
213
    }
214
 
215
    /**
216
     * Create a page from a source PDF.
217
     *
218
     * @param int $pageno
219
     */
220
    protected function create_page_from_source($pageno) {
221
        // Get the size (and deduce the orientation) of the next page.
222
        $template = $this->importPage($pageno);
223
        $size = $this->getTemplateSize($template);
224
 
225
        // Create a page of the required size / orientation.
226
        $this->AddPage($size['orientation'], array($size['width'], $size['height']));
227
        // Prevent new page creation when comments are at the bottom of a page.
228
        $this->setPageOrientation($size['orientation'], false, 0);
229
        // Fill in the page with the original contents from the student.
230
        $this->useTemplate($template);
231
    }
232
 
233
    /**
234
     * Copy all the remaining pages in the file
235
     */
236
    public function copy_remaining_pages() {
237
        $morepages = true;
238
        while ($morepages) {
239
            $morepages = $this->copy_page();
240
        }
241
    }
242
 
243
    /**
244
     * Append all comments to the end of the document.
245
     *
246
     * @param array $allcomments All comments, indexed by page number (starting at 0).
247
     * @return array|bool An array of links to comments, or false.
248
     */
249
    public function append_comments($allcomments) {
250
        if (!$this->filename) {
251
            return false;
252
        }
253
 
254
        $this->SetFontSize(12 * $this->scale);
255
        $this->SetMargins(100 * $this->scale, 120 * $this->scale, -1, true);
256
        $this->SetAutoPageBreak(true, 100 * $this->scale);
257
        $this->setHeaderFont(array($this->get_export_font_name(), '', 24 * $this->scale, '', true));
258
        $this->setHeaderMargin(24 * $this->scale);
259
        $this->setHeaderData('', 0, '', get_string('commentindex', 'assignfeedback_editpdf'));
260
 
261
        // Add a new page to the document with an appropriate header.
262
        $this->setPrintHeader(true);
263
        $this->AddPage();
264
 
265
        // Add the comments.
266
        $commentlinks = array();
267
        foreach ($allcomments as $pageno => $comments) {
268
            foreach ($comments as $index => $comment) {
269
                // Create a link to the current location, which will be added to the marker.
270
                $commentlink = $this->AddLink();
271
                $this->SetLink($commentlink, -1);
272
                $commentlinks[$pageno][$index] = $commentlink;
273
                // Also create a link back to the marker, which will be added here.
274
                $markerlink = $this->AddLink();
275
                $this->SetLink($markerlink, $comment->y * $this->scale, $pageno + 1);
276
                $label = get_string('commentlabel', 'assignfeedback_editpdf', array('pnum' => $pageno + 1, 'cnum' => $index + 1));
277
                $this->Cell(50 * $this->scale, 0, $label, 0, 0, '', false, $markerlink);
278
                $this->MultiCell(0, 0, $comment->rawtext, 0, 'L');
279
                $this->Ln(12 * $this->scale);
280
            }
281
            // Add an extra line break between pages.
282
            $this->Ln(12 * $this->scale);
283
        }
284
 
285
        return $commentlinks;
286
    }
287
 
288
    /**
289
     * Add a comment marker to the specified page.
290
     *
291
     * @param int $pageno The page number to add markers to (starting at 0).
292
     * @param int $index The comment index.
293
     * @param int $x The x-coordinate of the marker (in pixels).
294
     * @param int $y The y-coordinate of the marker (in pixels).
295
     * @param int $link The link identifier pointing to the full comment text.
296
     * @param string $colour The fill colour of the marker (red, yellow, green, blue, white, clear).
297
     * @return bool Success status.
298
     */
299
    public function add_comment_marker($pageno, $index, $x, $y, $link, $colour = 'yellow') {
300
        if (!$this->filename) {
301
            return false;
302
        }
303
 
304
        $fill = '';
305
        $fillopacity = 0.9;
306
        switch ($colour) {
307
            case 'red':
308
                $fill = 'rgb(249, 181, 179)';
309
                break;
310
            case 'green':
311
                $fill = 'rgb(214, 234, 178)';
312
                break;
313
            case 'blue':
314
                $fill = 'rgb(203, 217, 237)';
315
                break;
316
            case 'white':
317
                $fill = 'rgb(255, 255, 255)';
318
                break;
319
            case 'clear':
320
                $fillopacity = 0;
321
                break;
322
            default: /* Yellow */
323
                $fill = 'rgb(255, 236, 174)';
324
        }
325
        $marker = '@<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 12 12" preserveAspectRatio="xMinYMin meet">' .
326
                '<path d="M11 0H1C.4 0 0 .4 0 1v6c0 .6.4 1 1 1h1v4l4-4h5c.6 0 1-.4 1-1V1c0-.6-.4-1-1-1z" fill="' . $fill . '" ' .
327
                'fill-opacity="' . $fillopacity . '" stroke="rgb(153, 153, 153)" stroke-width="0.5"/></svg>';
328
        $label = get_string('commentlabel', 'assignfeedback_editpdf', array('pnum' => $pageno + 1, 'cnum' => $index + 1));
329
 
330
        $x *= $this->scale;
331
        $y *= $this->scale;
332
        $size = 24 * $this->scale;
333
        $this->SetDrawColor(51, 51, 51);
334
        $this->SetFontSize(10 * $this->scale);
335
        $this->setPage($pageno + 1);
336
 
337
        // Add the marker image.
338
        $this->ImageSVG($marker, $x - 0.5, $y - 0.5, $size, $size, $link);
339
 
340
        // Add the label.
341
        $this->MultiCell($size * 0.95, 0, $label, 0, 'C', false, 1, $x, $y, true, 0, false, true, $size * 0.60, 'M', true);
342
 
343
        return true;
344
    }
345
 
346
    /**
347
     * Add a comment to the current page
348
     * @param string $text the text of the comment
349
     * @param int $x the x-coordinate of the comment (in pixels)
350
     * @param int $y the y-coordinate of the comment (in pixels)
351
     * @param int $width the width of the comment (in pixels)
352
     * @param string $colour optional the background colour of the comment (red, yellow, green, blue, white, clear)
353
     * @return bool true if successful (always)
354
     */
355
    public function add_comment($text, $x, $y, $width, $colour = 'yellow') {
356
        if (!$this->filename) {
357
            return false;
358
        }
359
        $this->SetDrawColor(51, 51, 51);
360
        switch ($colour) {
361
            case 'red':
362
                $this->SetFillColor(249, 181, 179);
363
                break;
364
            case 'green':
365
                $this->SetFillColor(214, 234, 178);
366
                break;
367
            case 'blue':
368
                $this->SetFillColor(203, 217, 237);
369
                break;
370
            case 'white':
371
                $this->SetFillColor(255, 255, 255);
372
                break;
373
            default: /* Yellow */
374
                $this->SetFillColor(255, 236, 174);
375
                break;
376
        }
377
 
378
        $x *= $this->scale;
379
        $y *= $this->scale;
380
        $width *= $this->scale;
381
        $text = str_replace('&lt;', '<', $text);
382
        $text = str_replace('&gt;', '>', $text);
383
        // Draw the text with a border, but no background colour (using a background colour would cause the fill to
384
        // appear behind any existing content on the page, hence the extra filled rectangle drawn below).
385
        $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */
386
        if ($colour != 'clear') {
387
            $newy = $this->GetY();
388
            // Now we know the final size of the comment, draw a rectangle with the background colour.
389
            $this->Rect($x, $y, $width, $newy - $y, 'DF');
390
            // Re-draw the text over the top of the background rectangle.
391
            $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */
392
        }
393
        return true;
394
    }
395
 
396
    /**
397
     * Add an annotation to the current page
398
     * @param int $sx starting x-coordinate (in pixels)
399
     * @param int $sy starting y-coordinate (in pixels)
400
     * @param int $ex ending x-coordinate (in pixels)
401
     * @param int $ey ending y-coordinate (in pixels)
402
     * @param string $colour optional the colour of the annotation (red, yellow, green, blue, white, black)
403
     * @param string $type optional the type of annotation (line, oval, rectangle, highlight, pen, stamp)
404
     * @param int[]|string $path optional for 'pen' annotations this is an array of x and y coordinates for
405
     *              the line, for 'stamp' annotations it is the name of the stamp file (without the path)
406
     * @param string $imagefolder - Folder containing stamp images.
407
     * @return bool true if successful (always)
408
     */
409
    public function add_annotation($sx, $sy, $ex, $ey, $colour, $type, $path, $imagefolder) {
410
        global $CFG;
411
        if (!$this->filename) {
412
            return false;
413
        }
414
        switch ($colour) {
415
            case 'yellow':
416
                $colourarray = array(255, 207, 53);
417
                break;
418
            case 'green':
419
                $colourarray = array(153, 202, 62);
420
                break;
421
            case 'blue':
422
                $colourarray = array(125, 159, 211);
423
                break;
424
            case 'white':
425
                $colourarray = array(255, 255, 255);
426
                break;
427
            case 'black':
428
                $colourarray = array(51, 51, 51);
429
                break;
430
            default: /* Red */
431
                $colour = 'red';
432
                $colourarray = array(239, 69, 64);
433
                break;
434
        }
435
        $this->SetDrawColorArray($colourarray);
436
 
437
        $sx *= $this->scale;
438
        $sy *= $this->scale;
439
        $ex *= $this->scale;
440
        $ey *= $this->scale;
441
 
442
        $this->SetLineWidth(3.0 * $this->scale);
443
        switch ($type) {
444
            case 'oval':
445
                $rx = abs($sx - $ex) / 2;
446
                $ry = abs($sy - $ey) / 2;
447
                $sx = min($sx, $ex) + $rx;
448
                $sy = min($sy, $ey) + $ry;
449
 
450
                // $rx and $ry should be >= min width and height
451
                if ($rx < self::MIN_ANNOTATION_WIDTH) {
452
                    $rx = self::MIN_ANNOTATION_WIDTH;
453
                }
454
                if ($ry < self::MIN_ANNOTATION_HEIGHT) {
455
                    $ry = self::MIN_ANNOTATION_HEIGHT;
456
                }
457
 
458
                $this->Ellipse($sx, $sy, $rx, $ry);
459
                break;
460
            case 'rectangle':
461
                $w = abs($sx - $ex);
462
                $h = abs($sy - $ey);
463
                $sx = min($sx, $ex);
464
                $sy = min($sy, $ey);
465
 
466
                // Width or height should be >= min width and height
467
                if ($w < self::MIN_ANNOTATION_WIDTH) {
468
                    $w = self::MIN_ANNOTATION_WIDTH;
469
                }
470
                if ($h < self::MIN_ANNOTATION_HEIGHT) {
471
                    $h = self::MIN_ANNOTATION_HEIGHT;
472
                }
473
                $this->Rect($sx, $sy, $w, $h);
474
                break;
475
            case 'highlight':
476
                $w = abs($sx - $ex);
477
                $h = 8.0 * $this->scale;
478
                $sx = min($sx, $ex);
479
                $sy = min($sy, $ey) + ($h * 0.5);
480
                $this->SetAlpha(0.5, 'Normal', 0.5, 'Normal');
481
                $this->SetLineWidth(8.0 * $this->scale);
482
 
483
                // width should be >= min width
484
                if ($w < self::MIN_ANNOTATION_WIDTH) {
485
                    $w = self::MIN_ANNOTATION_WIDTH;
486
                }
487
 
488
                $this->Rect($sx, $sy, $w, $h);
489
                $this->SetAlpha(1.0, 'Normal', 1.0, 'Normal');
490
                break;
491
            case 'pen':
492
                if ($path) {
493
                    $scalepath = array();
494
                    $points = preg_split('/[,:]/', $path);
495
                    foreach ($points as $point) {
496
                        $scalepath[] = intval($point) * $this->scale;
497
                    }
498
 
499
                    if (!empty($scalepath)) {
500
                        $this->PolyLine($scalepath, 'S');
501
                    }
502
                }
503
                break;
504
            case 'stamp':
505
                $imgfile = $imagefolder . '/' . clean_filename($path);
506
                $w = abs($sx - $ex);
507
                $h = abs($sy - $ey);
508
                $sx = min($sx, $ex);
509
                $sy = min($sy, $ey);
510
 
511
                // Stamp is always more than 40px, so no need to check width/height.
512
                $this->Image($imgfile, $sx, $sy, $w, $h);
513
                break;
514
            default: // Line.
515
                $this->Line($sx, $sy, $ex, $ey);
516
                break;
517
        }
518
        $this->SetDrawColor(0, 0, 0);
519
        $this->SetLineWidth(1.0 * $this->scale);
520
 
521
        return true;
522
    }
523
 
524
    /**
525
     * Save the completed PDF to the given file
526
     * @param string $filename the filename for the PDF (including the full path)
527
     */
528
    public function save_pdf($filename) {
529
        $olddebug = error_reporting(0);
530
        $this->Output($filename, 'F');
531
        error_reporting($olddebug);
532
    }
533
 
534
    /**
535
     * Set the path to the folder in which to generate page image files
536
     * @param string $folder
537
     */
538
    public function set_image_folder($folder) {
539
        $this->imagefolder = $folder;
540
    }
541
 
542
    /**
543
     * Generate images from the PDF
544
     * @return array Array of filename of the generated images
545
     */
546
    public function get_images(): array {
547
        $this->precheck_generate_image();
548
 
549
        $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE;
550
        $command = $this->get_command_for_image(-1, $imagefile);
551
        exec($command);
552
        $images = array();
553
        for ($i = 0; $i < $this->pagecount; $i++) {
554
            // Image file is created from 1, so need to change to 0.
555
            $file = $imagefile . ($i + 1) . '.png';
556
            $newfile = $imagefile . $i . '.png';
557
            if (file_exists($file)) {
558
                rename($file, $newfile);
559
            } else {
560
                // Converter added '-' and zerofill for the pagenumber.
561
                $length = strlen($this->pagecount);
562
                $file = $imagefile . '-' . str_pad(($i + 1), $length, '0', STR_PAD_LEFT) . '.png';
563
                if (file_exists($file)) {
564
                    rename($file, $newfile);
565
                } else {
566
                    $newfile = self::get_error_image($this->imagefolder, $i);
567
                }
568
            }
569
            $images[$i] = basename($newfile);
570
        }
571
        return $images;
572
    }
573
 
574
    /**
575
     * Generate an image of the specified page in the PDF
576
     * @param int $pageno the page to generate the image of
577
     * @throws \moodle_exception
578
     * @throws \coding_exception
579
     * @return string the filename of the generated image
580
     */
581
    public function get_image($pageno) {
582
        $this->precheck_generate_image();
583
 
584
        $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE . $pageno . '.png';
585
        $generate = true;
586
        if (file_exists($imagefile)) {
587
            if (filemtime($imagefile) > filemtime($this->filename)) {
588
                // Make sure the image is newer than the PDF file.
589
                $generate = false;
590
            }
591
        }
592
 
593
        if ($generate) {
594
            $command = $this->get_command_for_image($pageno, $imagefile);
595
            $output = null;
596
            $result = exec($command, $output);
597
            if (!file_exists($imagefile)) {
598
                $fullerror = '<pre>'.get_string('command', 'assignfeedback_editpdf')."\n";
599
                $fullerror .= $command . "\n\n";
600
                $fullerror .= get_string('result', 'assignfeedback_editpdf')."\n";
601
                $fullerror .= htmlspecialchars($result, ENT_COMPAT) . "\n\n";
602
                $fullerror .= get_string('output', 'assignfeedback_editpdf')."\n";
603
                $fullerror .= htmlspecialchars(implode("\n", $output), ENT_COMPAT) . '</pre>';
604
                throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror);
605
            }
606
        }
607
 
608
        return self::IMAGE_PAGE . $pageno . '.png';
609
    }
610
 
611
    /**
612
     * Make sure the file name and image folder are ready before generate image.
613
     * @return bool
614
     */
615
    protected function precheck_generate_image() {
616
        if (!$this->filename) {
617
            throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename');
618
        }
619
 
620
        if (!$this->imagefolder) {
621
            throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder');
622
        }
623
 
624
        if (!is_dir($this->imagefolder)) {
625
            throw new \coding_exception('The specified image output folder is not a valid folder');
626
        }
627
        return true;
628
    }
629
 
630
    /**
631
     * Gets the command to use to extract as image the given $pageno page number
632
     * from a PDF document into the $imagefile file.
633
     * @param int $pageno Page number to extract from document. -1 means for all pages.
634
     * @param string $imagefile Target filename for the PNG image as absolute path.
635
     * @return string The command to use to extract a page as PNG image.
636
     */
637
    private function get_command_for_image(int $pageno, string $imagefile): string {
638
        global $CFG;
639
 
640
        // First, quickest convertion option.
641
        if (!empty($CFG->pathtopdftoppm) && is_executable($CFG->pathtopdftoppm)) {
642
            return $this->get_pdftoppm_command_for_image($pageno, $imagefile);
643
        }
644
 
645
        // Otherwise, rely on default behaviour.
646
        return $this->get_gs_command_for_image($pageno, $imagefile);
647
    }
648
 
649
    /**
650
     * Gets the pdftoppm command to use to extract as image the given $pageno page number
651
     * from a PDF document into the $imagefile file.
652
     * @param int $pageno Page number to extract from document. -1 means for all pages.
653
     * @param string $imagefile Target filename for the PNG image as absolute path.
654
     * @return string The pdftoppm command to use to extract a page as PNG image.
655
     */
656
    private function get_pdftoppm_command_for_image(int $pageno, string $imagefile): string {
657
        global $CFG;
658
        $pdftoppmexec = \escapeshellarg($CFG->pathtopdftoppm);
659
        $imageres = \escapeshellarg(100);
660
        $filename = \escapeshellarg($this->filename);
661
        $pagenoinc = \escapeshellarg($pageno + 1);
662
        if ($pageno >= 0) {
663
            // Convert 1 page.
664
            $imagefile = substr($imagefile, 0, -4); // Pdftoppm tool automatically adds extension file.
665
            $frompageno = $pagenoinc;
666
            $topageno = $pagenoinc;
667
            $singlefile = '-singlefile';
668
        } else {
669
            // Convert all pages at once.
670
            $frompageno = 1;
671
            $topageno = $this->pagecount;
672
            $singlefile = '';
673
        }
674
        $imagefilearg = \escapeshellarg($imagefile);
675
        return "$pdftoppmexec -q -r $imageres -f $frompageno -l $topageno -png $singlefile $filename $imagefilearg";
676
    }
677
 
678
    /**
679
     * Gets the ghostscript (gs) command to use to extract as image the given $pageno page number
680
     * from a PDF document into the $imagefile file.
681
     * @param int $pageno Page number to extract from document. -1 means for all pages.
682
     * @param string $imagefile Target filename for the PNG image as absolute path.
683
     * @return string The ghostscript (gs) command to use to extract a page as PNG image.
684
     */
685
    private function get_gs_command_for_image(int $pageno, string $imagefile): string {
686
        global $CFG;
687
        $gsexec = \escapeshellarg($CFG->pathtogs);
688
        $imageres = \escapeshellarg(100);
689
        $imagefilearg = \escapeshellarg($imagefile);
690
        $filename = \escapeshellarg($this->filename);
691
        $pagenoinc = \escapeshellarg($pageno + 1);
692
        if ($pageno >= 0) {
693
            // Convert 1 page.
694
            $firstpage = $pagenoinc;
695
            $lastpage = $pagenoinc;
696
        } else {
697
            // Convert all pages at once.
698
            $imagefilearg = \escapeshellarg($imagefile . '%d.png');
699
            $firstpage = 1;
700
            $lastpage = $this->pagecount;
701
        }
702
        return "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$firstpage -dLastPage=$lastpage ".
703
            "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename";
704
    }
705
 
706
    /**
707
     * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it
708
     *
709
     * @param \stored_file $file
710
     * @return string path to copy or converted pdf (false == fail)
711
     */
712
    public static function ensure_pdf_compatible(\stored_file $file) {
713
        global $CFG;
714
 
715
        // Copy the stored_file to local disk for checking.
716
        $temparea = make_request_directory();
717
        $tempsrc = $temparea . "/source.pdf";
718
        $file->copy_content_to($tempsrc);
719
 
720
        return self::ensure_pdf_file_compatible($tempsrc);
721
    }
722
 
723
    /**
724
     * Flatten and convert file using ghostscript then load pdf.
725
     *
726
     * @param   string $tempsrc The path to the file on disk.
727
     * @return  string path to copy or converted pdf (false == fail)
728
     */
729
    public static function ensure_pdf_file_compatible($tempsrc) {
730
        global $CFG;
731
 
732
        $temparea = make_request_directory();
733
        $tempdst = $temparea . "/target.pdf";
734
 
735
        $gsexec = \escapeshellarg($CFG->pathtogs);
736
        $tempdstarg = \escapeshellarg($tempdst);
737
        $tempsrcarg = \escapeshellarg($tempsrc);
1441 ariadna 738
        $command = "$gsexec -q -sDEVICE=pdfwrite -dPreserveAnnots=false -dSAFER -dBATCH -dNOPAUSE -dCompatibilityLevel=1.4 "
1 efrain 739
            . "-sOutputFile=$tempdstarg $tempsrcarg";
740
 
741
        exec($command);
742
        if (!file_exists($tempdst)) {
743
            // Something has gone wrong in the conversion.
744
            return false;
745
        }
746
 
747
        $pdf = new pdf();
748
        $pagecount = 0;
749
        try {
750
            $pagecount = $pdf->load_pdf($tempdst);
751
        } catch (\Exception $e) {
752
            // PDF was not valid - try running it through ghostscript to clean it up.
753
            $pagecount = 0;
754
        }
755
        $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
756
 
757
        if ($pagecount <= 0) {
758
            // Could not parse the converted pdf.
759
            return false;
760
        }
761
 
762
        return $tempdst;
763
    }
764
 
765
    /**
766
     * Generate an localised error image for the given pagenumber.
767
     *
768
     * @param string $errorimagefolder path of the folder where error image needs to be created.
769
     * @param int $pageno page number for which error image needs to be created.
770
     *
771
     * @return string File name
772
     * @throws \coding_exception
773
     */
774
    public static function get_error_image($errorimagefolder, $pageno) {
775
        global $CFG;
776
 
777
        $errorfile = $CFG->dirroot . self::BLANK_PDF;
778
        if (!file_exists($errorfile)) {
779
            throw new \coding_exception("Blank PDF not found", "File path" . $errorfile);
780
        }
781
 
782
        $tmperrorimagefolder = make_request_directory();
783
 
784
        $pdf = new pdf();
785
        $pdf->set_pdf($errorfile);
786
        $pdf->copy_page();
787
        $pdf->add_comment(get_string('errorpdfpage', 'assignfeedback_editpdf'), 250, 300, 200, "red");
788
        $generatedpdf = $tmperrorimagefolder . '/' . 'error.pdf';
789
        $pdf->save_pdf($generatedpdf);
790
 
791
        $pdf = new pdf();
792
        $pdf->set_pdf($generatedpdf);
793
        $pdf->set_image_folder($tmperrorimagefolder);
794
        $image = $pdf->get_image(0);
795
        $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
796
        $newimg = self::IMAGE_PAGE . $pageno . '.png';
797
 
798
        copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg);
799
        return $newimg;
800
    }
801
 
802
    /**
803
     * Test that the configured path to ghostscript is correct and working.
804
     * @param bool $generateimage - If true - a test image will be generated to verify the install.
805
     * @return \stdClass
806
     */
807
    public static function test_gs_path($generateimage = true) {
808
        global $CFG;
809
 
810
        $ret = (object)array(
811
            'status' => self::GSPATH_OK,
812
            'message' => null,
813
        );
814
        $gspath = $CFG->pathtogs;
815
        if (empty($gspath)) {
816
            $ret->status = self::GSPATH_EMPTY;
817
            return $ret;
818
        }
819
        if (!file_exists($gspath)) {
820
            $ret->status = self::GSPATH_DOESNOTEXIST;
821
            return $ret;
822
        }
823
        if (is_dir($gspath)) {
824
            $ret->status = self::GSPATH_ISDIR;
825
            return $ret;
826
        }
827
        if (!is_executable($gspath)) {
828
            $ret->status = self::GSPATH_NOTEXECUTABLE;
829
            return $ret;
830
        }
831
 
832
        if (!$generateimage) {
833
            return $ret;
834
        }
835
 
836
        $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf';
837
        if (!file_exists($testfile)) {
838
            $ret->status = self::GSPATH_NOTESTFILE;
839
            return $ret;
840
        }
841
 
842
        $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
843
        $filepath = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png';
844
        // Delete any previous test images, if they exist.
845
        if (file_exists($filepath)) {
846
            unlink($filepath);
847
        }
848
 
849
        $pdf = new pdf();
850
        $pdf->set_pdf($testfile);
851
        $pdf->set_image_folder($testimagefolder);
852
        try {
853
            $pdf->get_image(0);
854
        } catch (\moodle_exception $e) {
855
            $ret->status = self::GSPATH_ERROR;
856
            $ret->message = $e->getMessage();
857
        }
858
        $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
859
 
860
        return $ret;
861
    }
862
 
863
    /**
864
     * If the test image has been generated correctly - send it direct to the browser.
865
     */
866
    public static function send_test_image() {
867
        global $CFG;
868
        header('Content-type: image/png');
869
        require_once($CFG->libdir.'/filelib.php');
870
 
871
        $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
872
        $testimage = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png';
873
        send_file($testimage, basename($testimage), 0);
874
        die();
875
    }
876
 
877
    /**
878
     * This function add an image file to PDF page.
879
     * @param \stored_file $imagestoredfile Image file to be added
880
     */
881
    public function add_image_page($imagestoredfile) {
882
        $imageinfo = $imagestoredfile->get_imageinfo();
883
        $imagecontent = $imagestoredfile->get_content();
884
        $this->currentpage++;
885
        $template = $this->importPage($this->currentpage);
886
        $size = $this->getTemplateSize($template);
887
        $orientation = 'P';
888
        if ($imageinfo["width"] > $imageinfo["height"]) {
889
            if ($size['width'] < $size['height']) {
890
                $temp = $size['width'];
891
                $size['width'] = $size['height'];
892
                $size['height'] = $temp;
893
            }
894
            $orientation = 'L';
895
        } else if ($imageinfo["width"] < $imageinfo["height"]) {
896
            if ($size['width'] > $size['height']) {
897
                $temp = $size['width'];
898
                $size['width'] = $size['height'];
899
                $size['height'] = $temp;
900
            }
901
        }
902
 
903
        $this->SetHeaderMargin(0);
904
        $this->SetFooterMargin(0);
905
        $this->SetMargins(0, 0, 0, true);
906
        $this->setPrintFooter(false);
907
        $this->setPrintHeader(false);
908
 
909
        $this->AddPage($orientation, $size);
910
        $this->SetAutoPageBreak(false, 0);
911
        $this->Image('@' . $imagecontent, 0, 0, $size['width'], $size['height'],
912
            '', '', '', false, null, '', false, false, 0);
913
    }
914
}
915