MDL-74853 various: add second parameter to htmlentities functions
[moodle.git] / mod / assign / feedback / editpdf / classes / pdf.php
blob612d00007a910f6208fd92fb8ee844c5ed9ca8d3
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.
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
20 * @package assignfeedback_editpdf
21 * @copyright 2012 Davo Smith
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 namespace assignfeedback_editpdf;
26 use setasign\Fpdi\TcpdfFpdi;
28 defined('MOODLE_INTERNAL') || die();
30 global $CFG;
31 require_once($CFG->libdir.'/pdflib.php');
32 require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/autoload.php');
34 /**
35 * Library code for manipulating PDFs
37 * @package assignfeedback_editpdf
38 * @copyright 2012 Davo Smith
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 class pdf extends TcpdfFpdi {
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;
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.
81 * @return string
83 private function get_export_font_name() {
84 global $CFG;
86 $fontname = 'freesans';
87 if (!empty($CFG->pdfexportfont)) {
88 $fontname = $CFG->pdfexportfont;
90 return $fontname;
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
99 public function combine_pdfs($pdflist, $outfilename) {
101 raise_memory_limit(MEMORY_EXTRA);
102 $olddebug = error_reporting(0);
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);
112 $totalpagecount = 0;
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);
122 $this->save_pdf($outfilename);
123 error_reporting($olddebug);
125 return $totalpagecount;
129 * The number of the current page in the PDF being processed
130 * @return int
132 public function current_page() {
133 return $this->currentpage;
137 * The total number of pages in the PDF being processed
138 * @return int
140 public function page_count() {
141 return $this->pagecount;
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
150 public function load_pdf($filename) {
151 raise_memory_limit(MEMORY_EXTRA);
152 $olddebug = error_reporting(0);
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;
166 error_reporting($olddebug);
167 return $this->pagecount;
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
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;
189 * Copy the next page from the source file and set it as the current page
190 * @return bool true if successful
192 public function copy_page() {
193 if (!$this->filename) {
194 return false;
196 if ($this->currentpage>=$this->pagecount) {
197 return false;
199 $this->currentpage++;
200 $this->create_page_from_source($this->currentpage);
201 return true;
205 * Create a page from a source PDF.
207 * @param int $pageno
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);
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);
223 * Copy all the remaining pages in the file
225 public function copy_remaining_pages() {
226 $morepages = true;
227 while ($morepages) {
228 $morepages = $this->copy_page();
233 * Append all comments to the end of the document.
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.
238 public function append_comments($allcomments) {
239 if (!$this->filename) {
240 return false;
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'));
250 // Add a new page to the document with an appropriate header.
251 $this->setPrintHeader(true);
252 $this->AddPage();
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);
270 // Add an extra line break between pages.
271 $this->Ln(12 * $this->scale);
274 return $commentlinks;
278 * Add a comment marker to the specified page.
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.
288 public function add_comment_marker($pageno, $index, $x, $y, $link, $colour = 'yellow') {
289 if (!$this->filename) {
290 return false;
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)';
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));
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);
326 // Add the marker image.
327 $this->ImageSVG($marker, $x - 0.5, $y - 0.5, $size, $size, $link);
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);
332 return true;
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)
344 public function add_comment($text, $x, $y, $width, $colour = 'yellow') {
345 if (!$this->filename) {
346 return false;
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;
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 */
382 return true;
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)
398 public function add_annotation($sx, $sy, $ex, $ey, $colour, $type, $path, $imagefolder) {
399 global $CFG;
400 if (!$this->filename) {
401 return false;
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;
424 $this->SetDrawColorArray($colourarray);
426 $sx *= $this->scale;
427 $sy *= $this->scale;
428 $ex *= $this->scale;
429 $ey *= $this->scale;
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;
439 // $rx and $ry should be >= min width and height
440 if ($rx < self::MIN_ANNOTATION_WIDTH) {
441 $rx = self::MIN_ANNOTATION_WIDTH;
443 if ($ry < self::MIN_ANNOTATION_HEIGHT) {
444 $ry = self::MIN_ANNOTATION_HEIGHT;
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);
455 // Width or height should be >= min width and height
456 if ($w < self::MIN_ANNOTATION_WIDTH) {
457 $w = self::MIN_ANNOTATION_WIDTH;
459 if ($h < self::MIN_ANNOTATION_HEIGHT) {
460 $h = self::MIN_ANNOTATION_HEIGHT;
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);
472 // width should be >= min width
473 if ($w < self::MIN_ANNOTATION_WIDTH) {
474 $w = self::MIN_ANNOTATION_WIDTH;
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;
488 if (!empty($scalepath)) {
489 $this->PolyLine($scalepath, 'S');
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);
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;
507 $this->SetDrawColor(0, 0, 0);
508 $this->SetLineWidth(1.0 * $this->scale);
510 return true;
514 * Save the completed PDF to the given file
515 * @param string $filename the filename for the PDF (including the full path)
517 public function save_pdf($filename) {
518 $olddebug = error_reporting(0);
519 $this->Output($filename, 'F');
520 error_reporting($olddebug);
524 * Set the path to the folder in which to generate page image files
525 * @param string $folder
527 public function set_image_folder($folder) {
528 $this->imagefolder = $folder;
532 * Generate images from the PDF
533 * @return array Array of filename of the generated images
535 public function get_images(): array {
536 $this->precheck_generate_image();
538 $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE;
539 $command = $this->get_command_for_image(-1, $imagefile);
540 exec($command);
541 $images = array();
542 for ($i = 0; $i < $this->pagecount; $i++) {
543 // Image file is created from 1, so need to change to 0.
544 $file = $imagefile . ($i + 1) . '.png';
545 $newfile = $imagefile . $i . '.png';
546 if (file_exists($file)) {
547 rename($file, $newfile);
548 } else {
549 // Converter added '-' and zerofill for the pagenumber.
550 $length = strlen($this->pagecount);
551 $file = $imagefile . '-' . str_pad(($i + 1), $length, '0', STR_PAD_LEFT) . '.png';
552 if (file_exists($file)) {
553 rename($file, $newfile);
554 } else {
555 $newfile = self::get_error_image($this->imagefolder, $i);
558 $images[$i] = basename($newfile);
560 return $images;
564 * Generate an image of the specified page in the PDF
565 * @param int $pageno the page to generate the image of
566 * @throws \moodle_exception
567 * @throws \coding_exception
568 * @return string the filename of the generated image
570 public function get_image($pageno) {
571 $this->precheck_generate_image();
573 $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE . $pageno . '.png';
574 $generate = true;
575 if (file_exists($imagefile)) {
576 if (filemtime($imagefile) > filemtime($this->filename)) {
577 // Make sure the image is newer than the PDF file.
578 $generate = false;
582 if ($generate) {
583 $command = $this->get_command_for_image($pageno, $imagefile);
584 $output = null;
585 $result = exec($command, $output);
586 if (!file_exists($imagefile)) {
587 $fullerror = '<pre>'.get_string('command', 'assignfeedback_editpdf')."\n";
588 $fullerror .= $command . "\n\n";
589 $fullerror .= get_string('result', 'assignfeedback_editpdf')."\n";
590 $fullerror .= htmlspecialchars($result, ENT_COMPAT) . "\n\n";
591 $fullerror .= get_string('output', 'assignfeedback_editpdf')."\n";
592 $fullerror .= htmlspecialchars(implode("\n", $output), ENT_COMPAT) . '</pre>';
593 throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror);
597 return self::IMAGE_PAGE . $pageno . '.png';
601 * Make sure the file name and image folder are ready before generate image.
602 * @return bool
604 protected function precheck_generate_image() {
605 if (!$this->filename) {
606 throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename');
609 if (!$this->imagefolder) {
610 throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder');
613 if (!is_dir($this->imagefolder)) {
614 throw new \coding_exception('The specified image output folder is not a valid folder');
616 return true;
620 * Gets the command to use to extract as image the given $pageno page number
621 * from a PDF document into the $imagefile file.
622 * @param int $pageno Page number to extract from document. -1 means for all pages.
623 * @param string $imagefile Target filename for the PNG image as absolute path.
624 * @return string The command to use to extract a page as PNG image.
626 private function get_command_for_image(int $pageno, string $imagefile): string {
627 global $CFG;
629 // First, quickest convertion option.
630 if (!empty($CFG->pathtopdftoppm) && is_executable($CFG->pathtopdftoppm)) {
631 return $this->get_pdftoppm_command_for_image($pageno, $imagefile);
634 // Otherwise, rely on default behaviour.
635 return $this->get_gs_command_for_image($pageno, $imagefile);
639 * Gets the pdftoppm command to use to extract as image the given $pageno page number
640 * from a PDF document into the $imagefile file.
641 * @param int $pageno Page number to extract from document. -1 means for all pages.
642 * @param string $imagefile Target filename for the PNG image as absolute path.
643 * @return string The pdftoppm command to use to extract a page as PNG image.
645 private function get_pdftoppm_command_for_image(int $pageno, string $imagefile): string {
646 global $CFG;
647 $pdftoppmexec = \escapeshellarg($CFG->pathtopdftoppm);
648 $imageres = \escapeshellarg(100);
649 $filename = \escapeshellarg($this->filename);
650 $pagenoinc = \escapeshellarg($pageno + 1);
651 if ($pageno >= 0) {
652 // Convert 1 page.
653 $imagefile = substr($imagefile, 0, -4); // Pdftoppm tool automatically adds extension file.
654 $frompageno = $pagenoinc;
655 $topageno = $pagenoinc;
656 $singlefile = '-singlefile';
657 } else {
658 // Convert all pages at once.
659 $frompageno = 1;
660 $topageno = $this->pagecount;
661 $singlefile = '';
663 $imagefilearg = \escapeshellarg($imagefile);
664 return "$pdftoppmexec -q -r $imageres -f $frompageno -l $topageno -png $singlefile $filename $imagefilearg";
668 * Gets the ghostscript (gs) command to use to extract as image the given $pageno page number
669 * from a PDF document into the $imagefile file.
670 * @param int $pageno Page number to extract from document. -1 means for all pages.
671 * @param string $imagefile Target filename for the PNG image as absolute path.
672 * @return string The ghostscript (gs) command to use to extract a page as PNG image.
674 private function get_gs_command_for_image(int $pageno, string $imagefile): string {
675 global $CFG;
676 $gsexec = \escapeshellarg($CFG->pathtogs);
677 $imageres = \escapeshellarg(100);
678 $imagefilearg = \escapeshellarg($imagefile);
679 $filename = \escapeshellarg($this->filename);
680 $pagenoinc = \escapeshellarg($pageno + 1);
681 if ($pageno >= 0) {
682 // Convert 1 page.
683 $firstpage = $pagenoinc;
684 $lastpage = $pagenoinc;
685 } else {
686 // Convert all pages at once.
687 $imagefilearg = \escapeshellarg($imagefile . '%d.png');
688 $firstpage = 1;
689 $lastpage = $this->pagecount;
691 return "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$firstpage -dLastPage=$lastpage ".
692 "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename";
696 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it
698 * @param stored_file $file
699 * @return string path to copy or converted pdf (false == fail)
701 public static function ensure_pdf_compatible(\stored_file $file) {
702 global $CFG;
704 // Copy the stored_file to local disk for checking.
705 $temparea = make_request_directory();
706 $tempsrc = $temparea . "/source.pdf";
707 $file->copy_content_to($tempsrc);
709 return self::ensure_pdf_file_compatible($tempsrc);
713 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it
715 * @param string $tempsrc The path to the file on disk.
716 * @return string path to copy or converted pdf (false == fail)
718 public static function ensure_pdf_file_compatible($tempsrc) {
719 global $CFG;
721 $pdf = new pdf();
722 $pagecount = 0;
723 try {
724 $pagecount = $pdf->load_pdf($tempsrc);
725 } catch (\Exception $e) {
726 // PDF was not valid - try running it through ghostscript to clean it up.
727 $pagecount = 0;
729 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
731 if ($pagecount > 0) {
732 // PDF is already valid and can be read by tcpdf.
733 return $tempsrc;
736 $temparea = make_request_directory();
737 $tempdst = $temparea . "/target.pdf";
739 $gsexec = \escapeshellarg($CFG->pathtogs);
740 $tempdstarg = \escapeshellarg($tempdst);
741 $tempsrcarg = \escapeshellarg($tempsrc);
742 $command = "$gsexec -q -sDEVICE=pdfwrite -dSAFER -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
743 exec($command);
744 if (!file_exists($tempdst)) {
745 // Something has gone wrong in the conversion.
746 return false;
749 $pdf = new pdf();
750 $pagecount = 0;
751 try {
752 $pagecount = $pdf->load_pdf($tempdst);
753 } catch (\Exception $e) {
754 // PDF was not valid - try running it through ghostscript to clean it up.
755 $pagecount = 0;
757 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
759 if ($pagecount <= 0) {
760 // Could not parse the converted pdf.
761 return false;
764 return $tempdst;
768 * Generate an localised error image for the given pagenumber.
770 * @param string $errorimagefolder path of the folder where error image needs to be created.
771 * @param int $pageno page number for which error image needs to be created.
773 * @return string File name
774 * @throws \coding_exception
776 public static function get_error_image($errorimagefolder, $pageno) {
777 global $CFG;
779 $errorfile = $CFG->dirroot . self::BLANK_PDF;
780 if (!file_exists($errorfile)) {
781 throw new \coding_exception("Blank PDF not found", "File path" . $errorfile);
784 $tmperrorimagefolder = make_request_directory();
786 $pdf = new pdf();
787 $pdf->set_pdf($errorfile);
788 $pdf->copy_page();
789 $pdf->add_comment(get_string('errorpdfpage', 'assignfeedback_editpdf'), 250, 300, 200, "red");
790 $generatedpdf = $tmperrorimagefolder . '/' . 'error.pdf';
791 $pdf->save_pdf($generatedpdf);
793 $pdf = new pdf();
794 $pdf->set_pdf($generatedpdf);
795 $pdf->set_image_folder($tmperrorimagefolder);
796 $image = $pdf->get_image(0);
797 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
798 $newimg = self::IMAGE_PAGE . $pageno . '.png';
800 copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg);
801 return $newimg;
805 * Test that the configured path to ghostscript is correct and working.
806 * @param bool $generateimage - If true - a test image will be generated to verify the install.
807 * @return \stdClass
809 public static function test_gs_path($generateimage = true) {
810 global $CFG;
812 $ret = (object)array(
813 'status' => self::GSPATH_OK,
814 'message' => null,
816 $gspath = $CFG->pathtogs;
817 if (empty($gspath)) {
818 $ret->status = self::GSPATH_EMPTY;
819 return $ret;
821 if (!file_exists($gspath)) {
822 $ret->status = self::GSPATH_DOESNOTEXIST;
823 return $ret;
825 if (is_dir($gspath)) {
826 $ret->status = self::GSPATH_ISDIR;
827 return $ret;
829 if (!is_executable($gspath)) {
830 $ret->status = self::GSPATH_NOTEXECUTABLE;
831 return $ret;
834 if (!$generateimage) {
835 return $ret;
838 $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf';
839 if (!file_exists($testfile)) {
840 $ret->status = self::GSPATH_NOTESTFILE;
841 return $ret;
844 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
845 $filepath = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png';
846 // Delete any previous test images, if they exist.
847 if (file_exists($filepath)) {
848 unlink($filepath);
851 $pdf = new pdf();
852 $pdf->set_pdf($testfile);
853 $pdf->set_image_folder($testimagefolder);
854 try {
855 $pdf->get_image(0);
856 } catch (\moodle_exception $e) {
857 $ret->status = self::GSPATH_ERROR;
858 $ret->message = $e->getMessage();
860 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
862 return $ret;
866 * If the test image has been generated correctly - send it direct to the browser.
868 public static function send_test_image() {
869 global $CFG;
870 header('Content-type: image/png');
871 require_once($CFG->libdir.'/filelib.php');
873 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
874 $testimage = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png';
875 send_file($testimage, basename($testimage), 0);
876 die();
880 * This function add an image file to PDF page.
881 * @param \stored_file $imagestoredfile Image file to be added
883 public function add_image_page($imagestoredfile) {
884 $imageinfo = $imagestoredfile->get_imageinfo();
885 $imagecontent = $imagestoredfile->get_content();
886 $this->currentpage++;
887 $template = $this->importPage($this->currentpage);
888 $size = $this->getTemplateSize($template);
889 $orientation = 'P';
890 if ($imageinfo["width"] > $imageinfo["height"]) {
891 if ($size['width'] < $size['height']) {
892 $temp = $size['width'];
893 $size['width'] = $size['height'];
894 $size['height'] = $temp;
896 $orientation = 'L';
897 } else if ($imageinfo["width"] < $imageinfo["height"]) {
898 if ($size['width'] > $size['height']) {
899 $temp = $size['width'];
900 $size['width'] = $size['height'];
901 $size['height'] = $temp;
905 $this->SetHeaderMargin(0);
906 $this->SetFooterMargin(0);
907 $this->SetMargins(0, 0, 0, true);
908 $this->setPrintFooter(false);
909 $this->setPrintHeader(false);
911 $this->AddPage($orientation, $size);
912 $this->SetAutoPageBreak(false, 0);
913 $this->Image('@' . $imagecontent, 0, 0, $size['width'], $size['height'],
914 '', '', '', false, null, '', false, false, 0);