MDL-61537 assignfeedback_editpdf: Rotate PDF page
[moodle.git] / mod / assign / feedback / editpdf / classes / pdf.php
CommitLineData
5c386472
DW
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;
26
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30require_once($CFG->libdir.'/pdflib.php');
31require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/fpdi.php');
32
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 */
40class pdf extends \FPDI {
41
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;
52
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';
baf881b8
RT
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;
1f3556b0
AA
71 /** Blank PDF file used during error. */
72 const BLANK_PDF = '/mod/assign/feedback/editpdf/fixtures/blank.pdf';
9432553b
NN
73 /** Page image file name prefix*/
74 const IMAGE_PAGE = 'image_page';
490e48a4
DW
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;
84
85 $fontname = 'freesans';
86 if (!empty($CFG->pdfexportfont)) {
87 $fontname = $CFG->pdfexportfont;
88 }
89 return $fontname;
90 }
91
5c386472
DW
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) {
99
4fe9950b 100 raise_memory_limit(MEMORY_EXTRA);
ac2b4ffc 101 $olddebug = error_reporting(0);
4fe9950b 102
5c386472
DW
103 $this->setPageUnit('pt');
104 $this->setPrintHeader(false);
105 $this->setPrintFooter(false);
106 $this->scale = 72.0 / 100.0;
490e48a4
DW
107 // Use font supporting the widest range of characters.
108 $this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true);
5c386472
DW
109 $this->SetTextColor(0, 0, 0);
110
111 $totalpagecount = 0;
112
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 }
120
121 $this->save_pdf($outfilename);
ac2b4ffc 122 error_reporting($olddebug);
5c386472
DW
123
124 return $totalpagecount;
125 }
126
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 }
134
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 }
142
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) {
4fe9950b 150 raise_memory_limit(MEMORY_EXTRA);
ac2b4ffc 151 $olddebug = error_reporting(0);
4fe9950b 152
5c386472
DW
153 $this->setPageUnit('pt');
154 $this->scale = 72.0 / 100.0;
490e48a4 155 $this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true);
5c386472
DW
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;
4fe9950b 164
ac2b4ffc 165 error_reporting($olddebug);
5c386472
DW
166 return $this->pagecount;
167 }
168
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 }
186
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 }
202
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 }
223
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 }
233
2c153c56
TB
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 }
244
245 $this->SetFontSize(12 * $this->scale);
246 $this->SetMargins(100 * $this->scale, 120 * $this->scale, -1, true);
247 $this->SetAutoPageBreak(true, 100 * $this->scale);
490e48a4 248 $this->setHeaderFont(array($this->get_export_font_name(), '', 24 * $this->scale, '', true));
2c153c56
TB
249 $this->setHeaderMargin(24 * $this->scale);
250 $this->setHeaderData('', 0, '', get_string('commentindex', 'assignfeedback_editpdf'));
251
252 // Add a new page to the document with an appropriate header.
253 $this->setPrintHeader(true);
254 $this->AddPage();
255
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 }
275
276 return $commentlinks;
277 }
278
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 }
294
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));
320
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);
327
328 // Add the marker image.
329 $this->ImageSVG($marker, $x - 0.5, $y - 0.5, $size, $size, $link);
330
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);
333
334 return true;
335 }
336
5c386472
DW
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 }
1d38083c 350 $this->SetDrawColor(51, 51, 51);
5c386472
DW
351 switch ($colour) {
352 case 'red':
1d38083c 353 $this->SetFillColor(249, 181, 179);
5c386472
DW
354 break;
355 case 'green':
1d38083c 356 $this->SetFillColor(214, 234, 178);
5c386472
DW
357 break;
358 case 'blue':
1d38083c 359 $this->SetFillColor(203, 217, 237);
5c386472
DW
360 break;
361 case 'white':
362 $this->SetFillColor(255, 255, 255);
363 break;
364 default: /* Yellow */
1d38083c 365 $this->SetFillColor(255, 236, 174);
5c386472
DW
366 break;
367 }
368
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 }
386
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':
1d38083c 407 $colourarray = array(255, 207, 53);
5c386472
DW
408 break;
409 case 'green':
1d38083c 410 $colourarray = array(153, 202, 62);
5c386472
DW
411 break;
412 case 'blue':
1d38083c 413 $colourarray = array(125, 159, 211);
5c386472
DW
414 break;
415 case 'white':
416 $colourarray = array(255, 255, 255);
417 break;
418 case 'black':
1d38083c 419 $colourarray = array(51, 51, 51);
5c386472
DW
420 break;
421 default: /* Red */
422 $colour = 'red';
1d38083c 423 $colourarray = array(239, 69, 64);
5c386472
DW
424 break;
425 }
426 $this->SetDrawColorArray($colourarray);
427
428 $sx *= $this->scale;
429 $sy *= $this->scale;
430 $ex *= $this->scale;
431 $ey *= $this->scale;
432
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;
baf881b8
RT
440
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 }
448
5c386472
DW
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);
baf881b8
RT
456
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 }
5c386472
DW
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);
baf881b8
RT
473
474 // width should be >= min width
475 if ($w < self::MIN_ANNOTATION_WIDTH) {
476 $w = self::MIN_ANNOTATION_WIDTH;
477 }
478
5c386472
DW
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 }
baf881b8
RT
489
490 if (!empty($scalepath)) {
491 $this->PolyLine($scalepath, 'S');
492 }
5c386472
DW
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);
baf881b8
RT
501
502 // Stamp is always more than 40px, so no need to check width/height.
5c386472
DW
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);
511
512 return true;
513 }
514
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) {
ac2b4ffc 520 $olddebug = error_reporting(0);
5c386472 521 $this->Output($filename, 'F');
ac2b4ffc 522 error_reporting($olddebug);
5c386472
DW
523 }
524
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 }
532
533 /**
534 * Generate an image of the specified page in the PDF
535 * @param int $pageno the page to generate the image of
b6d06a5f
AA
536 * @throws \moodle_exception
537 * @throws \coding_exception
5c386472
DW
538 * @return string the filename of the generated image
539 */
540 public function get_image($pageno) {
1bce3a70
RT
541 global $CFG;
542
5c386472
DW
543 if (!$this->filename) {
544 throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename');
545 }
546
547 if (!$this->imagefolder) {
548 throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder');
549 }
550
551 if (!is_dir($this->imagefolder)) {
552 throw new \coding_exception('The specified image output folder is not a valid folder');
553 }
554
9432553b 555 $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE . $pageno . '.png';
5c386472
DW
556 $generate = true;
557 if (file_exists($imagefile)) {
b6d06a5f 558 if (filemtime($imagefile) > filemtime($this->filename)) {
5c386472
DW
559 // Make sure the image is newer than the PDF file.
560 $generate = false;
561 }
562 }
563
564 if ($generate) {
565 // Use ghostscript to generate an image of the specified page.
1bce3a70 566 $gsexec = \escapeshellarg($CFG->pathtogs);
1f738c8c
DW
567 $imageres = \escapeshellarg(100);
568 $imagefilearg = \escapeshellarg($imagefile);
569 $filename = \escapeshellarg($this->filename);
570 $pagenoinc = \escapeshellarg($pageno + 1);
5c386472 571 $command = "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$pagenoinc -dLastPage=$pagenoinc ".
626d8335 572 "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename";
5c386472 573
9092378d
DP
574 $output = null;
575 $result = exec($command, $output);
5c386472 576 if (!file_exists($imagefile)) {
9092378d
DP
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";
b6d06a5f 582 $fullerror .= htmlspecialchars(implode("\n", $output)) . '</pre>';
9092378d 583 throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror);
5c386472
DW
584 }
585 }
586
9432553b 587 return self::IMAGE_PAGE . $pageno . '.png';
5c386472
DW
588 }
589
590 /**
591 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it
f7a9f1dd
AN
592 *
593 * @param stored_file $file
5c386472
DW
594 * @return string path to copy or converted pdf (false == fail)
595 */
596 public static function ensure_pdf_compatible(\stored_file $file) {
1bce3a70
RT
597 global $CFG;
598
f7a9f1dd
AN
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);
603
604 return self::ensure_pdf_file_compatible($tempsrc);
605 }
606
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;
5c386472 615
ac2b4ffc
DW
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 }
a916d557 624 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
ac2b4ffc
DW
625
626 if ($pagecount > 0) {
f7a9f1dd 627 // PDF is already valid and can be read by tcpdf.
ac2b4ffc 628 return $tempsrc;
5c386472
DW
629 }
630
f7a9f1dd
AN
631 $temparea = make_request_directory();
632 $tempdst = $temparea . "/target.pdf";
633
1bce3a70 634 $gsexec = \escapeshellarg($CFG->pathtogs);
1f738c8c
DW
635 $tempdstarg = \escapeshellarg($tempdst);
636 $tempsrcarg = \escapeshellarg($tempsrc);
637 $command = "$gsexec -q -sDEVICE=pdfwrite -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
5c386472 638 exec($command);
5c386472
DW
639 if (!file_exists($tempdst)) {
640 // Something has gone wrong in the conversion.
641 return false;
642 }
643
ac2b4ffc
DW
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 }
a916d557
EL
652 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
653
ac2b4ffc 654 if ($pagecount <= 0) {
ac2b4ffc
DW
655 // Could not parse the converted pdf.
656 return false;
657 }
658
5c386472
DW
659 return $tempdst;
660 }
661
1f3556b0
AA
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;
673
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 }
678
679 $tmperrorimagefolder = make_request_directory();
680
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);
687
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.
9432553b 693 $newimg = self::IMAGE_PAGE . $pageno . '.png';
1f3556b0
AA
694
695 copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg);
696 return $newimg;
697 }
698
5c386472
DW
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.
b6d06a5f 702 * @return \stdClass
5c386472
DW
703 */
704 public static function test_gs_path($generateimage = true) {
705 global $CFG;
706
707 $ret = (object)array(
708 'status' => self::GSPATH_OK,
709 'message' => null,
710 );
1bce3a70 711 $gspath = $CFG->pathtogs;
5c386472
DW
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 }
728
9f7674bd 729 if (!$generateimage) {
5c386472
DW
730 return $ret;
731 }
732
9f7674bd
AO
733 $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf';
734 if (!file_exists($testfile)) {
735 $ret->status = self::GSPATH_NOTESTFILE;
5c386472
DW
736 return $ret;
737 }
738
739 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
9432553b 740 unlink($testimagefolder . '/' . self::IMAGE_PAGE . '0.png'); // Delete any previous test images.
5c386472
DW
741
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 }
a916d557 751 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
5c386472
DW
752
753 return $ret;
754 }
755
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');
763
764 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
9432553b 765 $testimage = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png';
0c431257 766 send_file($testimage, basename($testimage), 0);
5c386472
DW
767 die();
768 }
769
9432553b
NN
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);
780
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);
803
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 }
5c386472
DW
809}
810