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