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