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