Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Library code for manipulating PDFs 19 * 20 * @package assignfeedback_editpdf 21 * @copyright 2012 Davo Smith 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace assignfeedback_editpdf; 26 use setasign\Fpdi\TcpdfFpdi; 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 global $CFG; 31 require_once($CFG->libdir.'/pdflib.php'); 32 require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/autoload.php'); 33 34 /** 35 * Library code for manipulating PDFs 36 * 37 * @package assignfeedback_editpdf 38 * @copyright 2012 Davo Smith 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 */ 41 class pdf extends TcpdfFpdi { 42 43 /** @var int the number of the current page in the PDF being processed */ 44 protected $currentpage = 0; 45 /** @var int the total number of pages in the PDF being processed */ 46 protected $pagecount = 0; 47 /** @var float used to scale the pixel position of annotations (in the database) to the position in the final PDF */ 48 protected $scale = 0.0; 49 /** @var string the path in which to store generated page images */ 50 protected $imagefolder = null; 51 /** @var string the path to the PDF currently being processed */ 52 protected $filename = null; 53 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('<', '<', $text); 371 $text = str_replace('>', '>', $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, $type, $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 if (!$this->filename) { 540 throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename'); 541 } 542 543 if (!$this->imagefolder) { 544 throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder'); 545 } 546 547 if (!is_dir($this->imagefolder)) { 548 throw new \coding_exception('The specified image output folder is not a valid folder'); 549 } 550 551 $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE . $pageno . '.png'; 552 $generate = true; 553 if (file_exists($imagefile)) { 554 if (filemtime($imagefile) > filemtime($this->filename)) { 555 // Make sure the image is newer than the PDF file. 556 $generate = false; 557 } 558 } 559 560 if ($generate) { 561 $command = $this->get_command_for_image($pageno, $imagefile); 562 $output = null; 563 $result = exec($command, $output); 564 if (!file_exists($imagefile)) { 565 $fullerror = '<pre>'.get_string('command', 'assignfeedback_editpdf')."\n"; 566 $fullerror .= $command . "\n\n"; 567 $fullerror .= get_string('result', 'assignfeedback_editpdf')."\n"; 568 $fullerror .= htmlspecialchars($result) . "\n\n"; 569 $fullerror .= get_string('output', 'assignfeedback_editpdf')."\n"; 570 $fullerror .= htmlspecialchars(implode("\n", $output)) . '</pre>'; 571 throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror); 572 } 573 } 574 575 return self::IMAGE_PAGE . $pageno . '.png'; 576 } 577 578 /** 579 * Gets the command to use to extract as image the given $pageno page number 580 * from a PDF document into the $imagefile file. 581 * @param int $pageno Page number to extract from document. 582 * @param string $imagefile Target filename for the PNG image as absolute path. 583 * @return string The command to use to extract a page as PNG image. 584 */ 585 private function get_command_for_image(int $pageno, string $imagefile): string { 586 global $CFG; 587 588 // First, quickest convertion option. 589 if (!empty($CFG->pathtopdftoppm) && is_executable($CFG->pathtopdftoppm)) { 590 return $this->get_pdftoppm_command_for_image($pageno, $imagefile); 591 } 592 593 // Otherwise, rely on default behaviour. 594 return $this->get_gs_command_for_image($pageno, $imagefile); 595 } 596 597 /** 598 * Gets the pdftoppm command to use to extract as image the given $pageno page number 599 * from a PDF document into the $imagefile file. 600 * @param int $pageno Page number to extract from document. 601 * @param string $imagefile Target filename for the PNG image as absolute path. 602 * @return string The pdftoppm command to use to extract a page as PNG image. 603 */ 604 private function get_pdftoppm_command_for_image(int $pageno, string $imagefile): string { 605 global $CFG; 606 $pdftoppmexec = \escapeshellarg($CFG->pathtopdftoppm); 607 $imageres = \escapeshellarg(100); 608 $imagefile = substr($imagefile, 0, -4); // Pdftoppm tool automatically adds extension file. 609 $imagefilearg = \escapeshellarg($imagefile); 610 $filename = \escapeshellarg($this->filename); 611 $pagenoinc = \escapeshellarg($pageno + 1); 612 return "$pdftoppmexec -q -r $imageres -f $pagenoinc -l $pagenoinc -png -singlefile $filename $imagefilearg"; 613 } 614 615 /** 616 * Gets the ghostscript (gs) command to use to extract as image the given $pageno page number 617 * from a PDF document into the $imagefile file. 618 * @param int $pageno Page number to extract from document. 619 * @param string $imagefile Target filename for the PNG image as absolute path. 620 * @return string The ghostscript (gs) command to use to extract a page as PNG image. 621 */ 622 private function get_gs_command_for_image(int $pageno, string $imagefile): string { 623 global $CFG; 624 $gsexec = \escapeshellarg($CFG->pathtogs); 625 $imageres = \escapeshellarg(100); 626 $imagefilearg = \escapeshellarg($imagefile); 627 $filename = \escapeshellarg($this->filename); 628 $pagenoinc = \escapeshellarg($pageno + 1); 629 return "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$pagenoinc -dLastPage=$pagenoinc ". 630 "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename"; 631 } 632 633 /** 634 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it 635 * 636 * @param stored_file $file 637 * @return string path to copy or converted pdf (false == fail) 638 */ 639 public static function ensure_pdf_compatible(\stored_file $file) { 640 global $CFG; 641 642 // Copy the stored_file to local disk for checking. 643 $temparea = make_request_directory(); 644 $tempsrc = $temparea . "/source.pdf"; 645 $file->copy_content_to($tempsrc); 646 647 return self::ensure_pdf_file_compatible($tempsrc); 648 } 649 650 /** 651 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it 652 * 653 * @param string $tempsrc The path to the file on disk. 654 * @return string path to copy or converted pdf (false == fail) 655 */ 656 public static function ensure_pdf_file_compatible($tempsrc) { 657 global $CFG; 658 659 $pdf = new pdf(); 660 $pagecount = 0; 661 try { 662 $pagecount = $pdf->load_pdf($tempsrc); 663 } catch (\Exception $e) { 664 // PDF was not valid - try running it through ghostscript to clean it up. 665 $pagecount = 0; 666 } 667 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 668 669 if ($pagecount > 0) { 670 // PDF is already valid and can be read by tcpdf. 671 return $tempsrc; 672 } 673 674 $temparea = make_request_directory(); 675 $tempdst = $temparea . "/target.pdf"; 676 677 $gsexec = \escapeshellarg($CFG->pathtogs); 678 $tempdstarg = \escapeshellarg($tempdst); 679 $tempsrcarg = \escapeshellarg($tempsrc); 680 $command = "$gsexec -q -sDEVICE=pdfwrite -dSAFER -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg"; 681 exec($command); 682 if (!file_exists($tempdst)) { 683 // Something has gone wrong in the conversion. 684 return false; 685 } 686 687 $pdf = new pdf(); 688 $pagecount = 0; 689 try { 690 $pagecount = $pdf->load_pdf($tempdst); 691 } catch (\Exception $e) { 692 // PDF was not valid - try running it through ghostscript to clean it up. 693 $pagecount = 0; 694 } 695 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 696 697 if ($pagecount <= 0) { 698 // Could not parse the converted pdf. 699 return false; 700 } 701 702 return $tempdst; 703 } 704 705 /** 706 * Generate an localised error image for the given pagenumber. 707 * 708 * @param string $errorimagefolder path of the folder where error image needs to be created. 709 * @param int $pageno page number for which error image needs to be created. 710 * 711 * @return string File name 712 * @throws \coding_exception 713 */ 714 public static function get_error_image($errorimagefolder, $pageno) { 715 global $CFG; 716 717 $errorfile = $CFG->dirroot . self::BLANK_PDF; 718 if (!file_exists($errorfile)) { 719 throw new \coding_exception("Blank PDF not found", "File path" . $errorfile); 720 } 721 722 $tmperrorimagefolder = make_request_directory(); 723 724 $pdf = new pdf(); 725 $pdf->set_pdf($errorfile); 726 $pdf->copy_page(); 727 $pdf->add_comment(get_string('errorpdfpage', 'assignfeedback_editpdf'), 250, 300, 200, "red"); 728 $generatedpdf = $tmperrorimagefolder . '/' . 'error.pdf'; 729 $pdf->save_pdf($generatedpdf); 730 731 $pdf = new pdf(); 732 $pdf->set_pdf($generatedpdf); 733 $pdf->set_image_folder($tmperrorimagefolder); 734 $image = $pdf->get_image(0); 735 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 736 $newimg = self::IMAGE_PAGE . $pageno . '.png'; 737 738 copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg); 739 return $newimg; 740 } 741 742 /** 743 * Test that the configured path to ghostscript is correct and working. 744 * @param bool $generateimage - If true - a test image will be generated to verify the install. 745 * @return \stdClass 746 */ 747 public static function test_gs_path($generateimage = true) { 748 global $CFG; 749 750 $ret = (object)array( 751 'status' => self::GSPATH_OK, 752 'message' => null, 753 ); 754 $gspath = $CFG->pathtogs; 755 if (empty($gspath)) { 756 $ret->status = self::GSPATH_EMPTY; 757 return $ret; 758 } 759 if (!file_exists($gspath)) { 760 $ret->status = self::GSPATH_DOESNOTEXIST; 761 return $ret; 762 } 763 if (is_dir($gspath)) { 764 $ret->status = self::GSPATH_ISDIR; 765 return $ret; 766 } 767 if (!is_executable($gspath)) { 768 $ret->status = self::GSPATH_NOTEXECUTABLE; 769 return $ret; 770 } 771 772 if (!$generateimage) { 773 return $ret; 774 } 775 776 $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf'; 777 if (!file_exists($testfile)) { 778 $ret->status = self::GSPATH_NOTESTFILE; 779 return $ret; 780 } 781 782 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); 783 $filepath = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png'; 784 // Delete any previous test images, if they exist. 785 if (file_exists($filepath)) { 786 unlink($filepath); 787 } 788 789 $pdf = new pdf(); 790 $pdf->set_pdf($testfile); 791 $pdf->set_image_folder($testimagefolder); 792 try { 793 $pdf->get_image(0); 794 } catch (\moodle_exception $e) { 795 $ret->status = self::GSPATH_ERROR; 796 $ret->message = $e->getMessage(); 797 } 798 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 799 800 return $ret; 801 } 802 803 /** 804 * If the test image has been generated correctly - send it direct to the browser. 805 */ 806 public static function send_test_image() { 807 global $CFG; 808 header('Content-type: image/png'); 809 require_once($CFG->libdir.'/filelib.php'); 810 811 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); 812 $testimage = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png'; 813 send_file($testimage, basename($testimage), 0); 814 die(); 815 } 816 817 /** 818 * This function add an image file to PDF page. 819 * @param \stored_file $imagestoredfile Image file to be added 820 */ 821 public function add_image_page($imagestoredfile) { 822 $imageinfo = $imagestoredfile->get_imageinfo(); 823 $imagecontent = $imagestoredfile->get_content(); 824 $this->currentpage++; 825 $template = $this->importPage($this->currentpage); 826 $size = $this->getTemplateSize($template); 827 $orientation = 'P'; 828 if ($imageinfo["width"] > $imageinfo["height"]) { 829 if ($size['width'] < $size['height']) { 830 $temp = $size['width']; 831 $size['width'] = $size['height']; 832 $size['height'] = $temp; 833 } 834 $orientation = 'L'; 835 } else if ($imageinfo["width"] < $imageinfo["height"]) { 836 if ($size['width'] > $size['height']) { 837 $temp = $size['width']; 838 $size['width'] = $size['height']; 839 $size['height'] = $temp; 840 } 841 } 842 843 $this->SetHeaderMargin(0); 844 $this->SetFooterMargin(0); 845 $this->SetMargins(0, 0, 0, true); 846 $this->setPrintFooter(false); 847 $this->setPrintHeader(false); 848 849 $this->AddPage($orientation, $size); 850 $this->SetAutoPageBreak(false, 0); 851 $this->Image('@' . $imagecontent, 0, 0, $size['width'], $size['height'], 852 '', '', '', false, null, '', false, false, 0); 853 } 854 } 855
title
Description
Body
title
Description
Body
title
Description
Body
title
Body