See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
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 images from the PDF 533 * @return array Array of filename of the generated images 534 */ 535 public function get_images(): array { 536 $this->precheck_generate_image(); 537 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); 556 } 557 } 558 $images[$i] = basename($newfile); 559 } 560 return $images; 561 } 562 563 /** 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 569 */ 570 public function get_image($pageno) { 571 $this->precheck_generate_image(); 572 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; 579 } 580 } 581 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); 594 } 595 } 596 597 return self::IMAGE_PAGE . $pageno . '.png'; 598 } 599 600 /** 601 * Make sure the file name and image folder are ready before generate image. 602 * @return bool 603 */ 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'); 607 } 608 609 if (!$this->imagefolder) { 610 throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder'); 611 } 612 613 if (!is_dir($this->imagefolder)) { 614 throw new \coding_exception('The specified image output folder is not a valid folder'); 615 } 616 return true; 617 } 618 619 /** 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. 625 */ 626 private function get_command_for_image(int $pageno, string $imagefile): string { 627 global $CFG; 628 629 // First, quickest convertion option. 630 if (!empty($CFG->pathtopdftoppm) && is_executable($CFG->pathtopdftoppm)) { 631 return $this->get_pdftoppm_command_for_image($pageno, $imagefile); 632 } 633 634 // Otherwise, rely on default behaviour. 635 return $this->get_gs_command_for_image($pageno, $imagefile); 636 } 637 638 /** 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. 644 */ 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 = ''; 662 } 663 $imagefilearg = \escapeshellarg($imagefile); 664 return "$pdftoppmexec -q -r $imageres -f $frompageno -l $topageno -png $singlefile $filename $imagefilearg"; 665 } 666 667 /** 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. 673 */ 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; 690 } 691 return "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$firstpage -dLastPage=$lastpage ". 692 "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename"; 693 } 694 695 /** 696 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it 697 * 698 * @param stored_file $file 699 * @return string path to copy or converted pdf (false == fail) 700 */ 701 public static function ensure_pdf_compatible(\stored_file $file) { 702 global $CFG; 703 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); 708 709 return self::ensure_pdf_file_compatible($tempsrc); 710 } 711 712 /** 713 * Flatten and convert file using ghostscript then load pdf. 714 * 715 * @param string $tempsrc The path to the file on disk. 716 * @return string path to copy or converted pdf (false == fail) 717 */ 718 public static function ensure_pdf_file_compatible($tempsrc) { 719 global $CFG; 720 721 $temparea = make_request_directory(); 722 $tempdst = $temparea . "/target.pdf"; 723 724 $gsexec = \escapeshellarg($CFG->pathtogs); 725 $tempdstarg = \escapeshellarg($tempdst); 726 $tempsrcarg = \escapeshellarg($tempsrc); 727 $command = "$gsexec -q -sDEVICE=pdfwrite -dPreserveAnnots=false -dSAFER -dBATCH -dNOPAUSE " 728 . "-sOutputFile=$tempdstarg $tempsrcarg"; 729 730 exec($command); 731 if (!file_exists($tempdst)) { 732 // Something has gone wrong in the conversion. 733 return false; 734 } 735 736 $pdf = new pdf(); 737 $pagecount = 0; 738 try { 739 $pagecount = $pdf->load_pdf($tempdst); 740 } catch (\Exception $e) { 741 // PDF was not valid - try running it through ghostscript to clean it up. 742 $pagecount = 0; 743 } 744 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 745 746 if ($pagecount <= 0) { 747 // Could not parse the converted pdf. 748 return false; 749 } 750 751 return $tempdst; 752 } 753 754 /** 755 * Generate an localised error image for the given pagenumber. 756 * 757 * @param string $errorimagefolder path of the folder where error image needs to be created. 758 * @param int $pageno page number for which error image needs to be created. 759 * 760 * @return string File name 761 * @throws \coding_exception 762 */ 763 public static function get_error_image($errorimagefolder, $pageno) { 764 global $CFG; 765 766 $errorfile = $CFG->dirroot . self::BLANK_PDF; 767 if (!file_exists($errorfile)) { 768 throw new \coding_exception("Blank PDF not found", "File path" . $errorfile); 769 } 770 771 $tmperrorimagefolder = make_request_directory(); 772 773 $pdf = new pdf(); 774 $pdf->set_pdf($errorfile); 775 $pdf->copy_page(); 776 $pdf->add_comment(get_string('errorpdfpage', 'assignfeedback_editpdf'), 250, 300, 200, "red"); 777 $generatedpdf = $tmperrorimagefolder . '/' . 'error.pdf'; 778 $pdf->save_pdf($generatedpdf); 779 780 $pdf = new pdf(); 781 $pdf->set_pdf($generatedpdf); 782 $pdf->set_image_folder($tmperrorimagefolder); 783 $image = $pdf->get_image(0); 784 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 785 $newimg = self::IMAGE_PAGE . $pageno . '.png'; 786 787 copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg); 788 return $newimg; 789 } 790 791 /** 792 * Test that the configured path to ghostscript is correct and working. 793 * @param bool $generateimage - If true - a test image will be generated to verify the install. 794 * @return \stdClass 795 */ 796 public static function test_gs_path($generateimage = true) { 797 global $CFG; 798 799 $ret = (object)array( 800 'status' => self::GSPATH_OK, 801 'message' => null, 802 ); 803 $gspath = $CFG->pathtogs; 804 if (empty($gspath)) { 805 $ret->status = self::GSPATH_EMPTY; 806 return $ret; 807 } 808 if (!file_exists($gspath)) { 809 $ret->status = self::GSPATH_DOESNOTEXIST; 810 return $ret; 811 } 812 if (is_dir($gspath)) { 813 $ret->status = self::GSPATH_ISDIR; 814 return $ret; 815 } 816 if (!is_executable($gspath)) { 817 $ret->status = self::GSPATH_NOTEXECUTABLE; 818 return $ret; 819 } 820 821 if (!$generateimage) { 822 return $ret; 823 } 824 825 $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf'; 826 if (!file_exists($testfile)) { 827 $ret->status = self::GSPATH_NOTESTFILE; 828 return $ret; 829 } 830 831 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); 832 $filepath = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png'; 833 // Delete any previous test images, if they exist. 834 if (file_exists($filepath)) { 835 unlink($filepath); 836 } 837 838 $pdf = new pdf(); 839 $pdf->set_pdf($testfile); 840 $pdf->set_image_folder($testimagefolder); 841 try { 842 $pdf->get_image(0); 843 } catch (\moodle_exception $e) { 844 $ret->status = self::GSPATH_ERROR; 845 $ret->message = $e->getMessage(); 846 } 847 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 848 849 return $ret; 850 } 851 852 /** 853 * If the test image has been generated correctly - send it direct to the browser. 854 */ 855 public static function send_test_image() { 856 global $CFG; 857 header('Content-type: image/png'); 858 require_once($CFG->libdir.'/filelib.php'); 859 860 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); 861 $testimage = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png'; 862 send_file($testimage, basename($testimage), 0); 863 die(); 864 } 865 866 /** 867 * This function add an image file to PDF page. 868 * @param \stored_file $imagestoredfile Image file to be added 869 */ 870 public function add_image_page($imagestoredfile) { 871 $imageinfo = $imagestoredfile->get_imageinfo(); 872 $imagecontent = $imagestoredfile->get_content(); 873 $this->currentpage++; 874 $template = $this->importPage($this->currentpage); 875 $size = $this->getTemplateSize($template); 876 $orientation = 'P'; 877 if ($imageinfo["width"] > $imageinfo["height"]) { 878 if ($size['width'] < $size['height']) { 879 $temp = $size['width']; 880 $size['width'] = $size['height']; 881 $size['height'] = $temp; 882 } 883 $orientation = 'L'; 884 } else if ($imageinfo["width"] < $imageinfo["height"]) { 885 if ($size['width'] > $size['height']) { 886 $temp = $size['width']; 887 $size['width'] = $size['height']; 888 $size['height'] = $temp; 889 } 890 } 891 892 $this->SetHeaderMargin(0); 893 $this->SetFooterMargin(0); 894 $this->SetMargins(0, 0, 0, true); 895 $this->setPrintFooter(false); 896 $this->setPrintHeader(false); 897 898 $this->AddPage($orientation, $size); 899 $this->SetAutoPageBreak(false, 0); 900 $this->Image('@' . $imagecontent, 0, 0, $size['width'], $size['height'], 901 '', '', '', false, null, '', false, false, 0); 902 } 903 } 904
title
Description
Body
title
Description
Body
title
Description
Body
title
Body