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