[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; 30 require_once($CFG->libdir.'/pdflib.php'); 31 require_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 */ 40 class 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'; 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; 71 72 /** 73 * Combine the given PDF files into a single PDF. Optionally add a coversheet and coversheet fields. 74 * @param string[] $pdflist the filenames of the files to combine 75 * @param string $outfilename the filename to write to 76 * @return int the number of pages in the combined PDF 77 */ 78 public function combine_pdfs($pdflist, $outfilename) { 79 80 raise_memory_limit(MEMORY_EXTRA); 81 $olddebug = error_reporting(0); 82 83 $this->setPageUnit('pt'); 84 $this->setPrintHeader(false); 85 $this->setPrintFooter(false); 86 $this->scale = 72.0 / 100.0; 87 $this->SetFont('helvetica', '', 16.0 * $this->scale); 88 $this->SetTextColor(0, 0, 0); 89 90 $totalpagecount = 0; 91 92 foreach ($pdflist as $file) { 93 $pagecount = $this->setSourceFile($file); 94 $totalpagecount += $pagecount; 95 for ($i = 1; $i<=$pagecount; $i++) { 96 $this->create_page_from_source($i); 97 } 98 } 99 100 $this->save_pdf($outfilename); 101 error_reporting($olddebug); 102 103 return $totalpagecount; 104 } 105 106 /** 107 * The number of the current page in the PDF being processed 108 * @return int 109 */ 110 public function current_page() { 111 return $this->currentpage; 112 } 113 114 /** 115 * The total number of pages in the PDF being processed 116 * @return int 117 */ 118 public function page_count() { 119 return $this->pagecount; 120 } 121 122 /** 123 * Load the specified PDF and set the initial output configuration 124 * Used when processing comments and outputting a new PDF 125 * @param string $filename the path to the PDF to load 126 * @return int the number of pages in the PDF 127 */ 128 public function load_pdf($filename) { 129 raise_memory_limit(MEMORY_EXTRA); 130 $olddebug = error_reporting(0); 131 132 $this->setPageUnit('pt'); 133 $this->scale = 72.0 / 100.0; 134 $this->SetFont('helvetica', '', 16.0 * $this->scale); 135 $this->SetFillColor(255, 255, 176); 136 $this->SetDrawColor(0, 0, 0); 137 $this->SetLineWidth(1.0 * $this->scale); 138 $this->SetTextColor(0, 0, 0); 139 $this->setPrintHeader(false); 140 $this->setPrintFooter(false); 141 $this->pagecount = $this->setSourceFile($filename); 142 $this->filename = $filename; 143 144 error_reporting($olddebug); 145 return $this->pagecount; 146 } 147 148 /** 149 * Sets the name of the PDF to process, but only loads the file if the 150 * pagecount is zero (in order to count the number of pages) 151 * Used when generating page images (but not a new PDF) 152 * @param string $filename the path to the PDF to process 153 * @param int $pagecount optional the number of pages in the PDF, if known 154 * @return int the number of pages in the PDF 155 */ 156 public function set_pdf($filename, $pagecount = 0) { 157 if ($pagecount == 0) { 158 return $this->load_pdf($filename); 159 } else { 160 $this->filename = $filename; 161 $this->pagecount = $pagecount; 162 return $pagecount; 163 } 164 } 165 166 /** 167 * Copy the next page from the source file and set it as the current page 168 * @return bool true if successful 169 */ 170 public function copy_page() { 171 if (!$this->filename) { 172 return false; 173 } 174 if ($this->currentpage>=$this->pagecount) { 175 return false; 176 } 177 $this->currentpage++; 178 $this->create_page_from_source($this->currentpage); 179 return true; 180 } 181 182 /** 183 * Create a page from a source PDF. 184 * 185 * @param int $pageno 186 */ 187 protected function create_page_from_source($pageno) { 188 // Get the size (and deduce the orientation) of the next page. 189 $template = $this->importPage($pageno); 190 $size = $this->getTemplateSize($template); 191 $orientation = 'P'; 192 if ($size['w'] > $size['h']) { 193 $orientation = 'L'; 194 } 195 // Create a page of the required size / orientation. 196 $this->AddPage($orientation, array($size['w'], $size['h'])); 197 // Prevent new page creation when comments are at the bottom of a page. 198 $this->setPageOrientation($orientation, false, 0); 199 // Fill in the page with the original contents from the student. 200 $this->useTemplate($template); 201 } 202 203 /** 204 * Copy all the remaining pages in the file 205 */ 206 public function copy_remaining_pages() { 207 $morepages = true; 208 while ($morepages) { 209 $morepages = $this->copy_page(); 210 } 211 } 212 213 /** 214 * Add a comment to the current page 215 * @param string $text the text of the comment 216 * @param int $x the x-coordinate of the comment (in pixels) 217 * @param int $y the y-coordinate of the comment (in pixels) 218 * @param int $width the width of the comment (in pixels) 219 * @param string $colour optional the background colour of the comment (red, yellow, green, blue, white, clear) 220 * @return bool true if successful (always) 221 */ 222 public function add_comment($text, $x, $y, $width, $colour = 'yellow') { 223 if (!$this->filename) { 224 return false; 225 } 226 $this->SetDrawColor(51, 51, 51); 227 switch ($colour) { 228 case 'red': 229 $this->SetFillColor(249, 181, 179); 230 break; 231 case 'green': 232 $this->SetFillColor(214, 234, 178); 233 break; 234 case 'blue': 235 $this->SetFillColor(203, 217, 237); 236 break; 237 case 'white': 238 $this->SetFillColor(255, 255, 255); 239 break; 240 default: /* Yellow */ 241 $this->SetFillColor(255, 236, 174); 242 break; 243 } 244 245 $x *= $this->scale; 246 $y *= $this->scale; 247 $width *= $this->scale; 248 $text = str_replace('<', '<', $text); 249 $text = str_replace('>', '>', $text); 250 // Draw the text with a border, but no background colour (using a background colour would cause the fill to 251 // appear behind any existing content on the page, hence the extra filled rectangle drawn below). 252 $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */ 253 if ($colour != 'clear') { 254 $newy = $this->GetY(); 255 // Now we know the final size of the comment, draw a rectangle with the background colour. 256 $this->Rect($x, $y, $width, $newy - $y, 'DF'); 257 // Re-draw the text over the top of the background rectangle. 258 $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */ 259 } 260 return true; 261 } 262 263 /** 264 * Add an annotation to the current page 265 * @param int $sx starting x-coordinate (in pixels) 266 * @param int $sy starting y-coordinate (in pixels) 267 * @param int $ex ending x-coordinate (in pixels) 268 * @param int $ey ending y-coordinate (in pixels) 269 * @param string $colour optional the colour of the annotation (red, yellow, green, blue, white, black) 270 * @param string $type optional the type of annotation (line, oval, rectangle, highlight, pen, stamp) 271 * @param int[]|string $path optional for 'pen' annotations this is an array of x and y coordinates for 272 * the line, for 'stamp' annotations it is the name of the stamp file (without the path) 273 * @param string $imagefolder - Folder containing stamp images. 274 * @return bool true if successful (always) 275 */ 276 public function add_annotation($sx, $sy, $ex, $ey, $colour = 'yellow', $type = 'line', $path, $imagefolder) { 277 global $CFG; 278 if (!$this->filename) { 279 return false; 280 } 281 switch ($colour) { 282 case 'yellow': 283 $colourarray = array(255, 207, 53); 284 break; 285 case 'green': 286 $colourarray = array(153, 202, 62); 287 break; 288 case 'blue': 289 $colourarray = array(125, 159, 211); 290 break; 291 case 'white': 292 $colourarray = array(255, 255, 255); 293 break; 294 case 'black': 295 $colourarray = array(51, 51, 51); 296 break; 297 default: /* Red */ 298 $colour = 'red'; 299 $colourarray = array(239, 69, 64); 300 break; 301 } 302 $this->SetDrawColorArray($colourarray); 303 304 $sx *= $this->scale; 305 $sy *= $this->scale; 306 $ex *= $this->scale; 307 $ey *= $this->scale; 308 309 $this->SetLineWidth(3.0 * $this->scale); 310 switch ($type) { 311 case 'oval': 312 $rx = abs($sx - $ex) / 2; 313 $ry = abs($sy - $ey) / 2; 314 $sx = min($sx, $ex) + $rx; 315 $sy = min($sy, $ey) + $ry; 316 317 // $rx and $ry should be >= min width and height 318 if ($rx < self::MIN_ANNOTATION_WIDTH) { 319 $rx = self::MIN_ANNOTATION_WIDTH; 320 } 321 if ($ry < self::MIN_ANNOTATION_HEIGHT) { 322 $ry = self::MIN_ANNOTATION_HEIGHT; 323 } 324 325 $this->Ellipse($sx, $sy, $rx, $ry); 326 break; 327 case 'rectangle': 328 $w = abs($sx - $ex); 329 $h = abs($sy - $ey); 330 $sx = min($sx, $ex); 331 $sy = min($sy, $ey); 332 333 // Width or height should be >= min width and height 334 if ($w < self::MIN_ANNOTATION_WIDTH) { 335 $w = self::MIN_ANNOTATION_WIDTH; 336 } 337 if ($h < self::MIN_ANNOTATION_HEIGHT) { 338 $h = self::MIN_ANNOTATION_HEIGHT; 339 } 340 $this->Rect($sx, $sy, $w, $h); 341 break; 342 case 'highlight': 343 $w = abs($sx - $ex); 344 $h = 8.0 * $this->scale; 345 $sx = min($sx, $ex); 346 $sy = min($sy, $ey) + ($h * 0.5); 347 $this->SetAlpha(0.5, 'Normal', 0.5, 'Normal'); 348 $this->SetLineWidth(8.0 * $this->scale); 349 350 // width should be >= min width 351 if ($w < self::MIN_ANNOTATION_WIDTH) { 352 $w = self::MIN_ANNOTATION_WIDTH; 353 } 354 355 $this->Rect($sx, $sy, $w, $h); 356 $this->SetAlpha(1.0, 'Normal', 1.0, 'Normal'); 357 break; 358 case 'pen': 359 if ($path) { 360 $scalepath = array(); 361 $points = preg_split('/[,:]/', $path); 362 foreach ($points as $point) { 363 $scalepath[] = intval($point) * $this->scale; 364 } 365 366 if (!empty($scalepath)) { 367 $this->PolyLine($scalepath, 'S'); 368 } 369 } 370 break; 371 case 'stamp': 372 $imgfile = $imagefolder . '/' . clean_filename($path); 373 $w = abs($sx - $ex); 374 $h = abs($sy - $ey); 375 $sx = min($sx, $ex); 376 $sy = min($sy, $ey); 377 378 // Stamp is always more than 40px, so no need to check width/height. 379 $this->Image($imgfile, $sx, $sy, $w, $h); 380 break; 381 default: // Line. 382 $this->Line($sx, $sy, $ex, $ey); 383 break; 384 } 385 $this->SetDrawColor(0, 0, 0); 386 $this->SetLineWidth(1.0 * $this->scale); 387 388 return true; 389 } 390 391 /** 392 * Save the completed PDF to the given file 393 * @param string $filename the filename for the PDF (including the full path) 394 */ 395 public function save_pdf($filename) { 396 $olddebug = error_reporting(0); 397 $this->Output($filename, 'F'); 398 error_reporting($olddebug); 399 } 400 401 /** 402 * Set the path to the folder in which to generate page image files 403 * @param string $folder 404 */ 405 public function set_image_folder($folder) { 406 $this->imagefolder = $folder; 407 } 408 409 /** 410 * Generate an image of the specified page in the PDF 411 * @param int $pageno the page to generate the image of 412 * @throws moodle_exception 413 * @throws coding_exception 414 * @return string the filename of the generated image 415 */ 416 public function get_image($pageno) { 417 global $CFG; 418 419 if (!$this->filename) { 420 throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename'); 421 } 422 423 if (!$this->imagefolder) { 424 throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder'); 425 } 426 427 if (!is_dir($this->imagefolder)) { 428 throw new \coding_exception('The specified image output folder is not a valid folder'); 429 } 430 431 $imagefile = $this->imagefolder.'/image_page' . $pageno . '.png'; 432 $generate = true; 433 if (file_exists($imagefile)) { 434 if (filemtime($imagefile)>filemtime($this->filename)) { 435 // Make sure the image is newer than the PDF file. 436 $generate = false; 437 } 438 } 439 440 if ($generate) { 441 // Use ghostscript to generate an image of the specified page. 442 $gsexec = \escapeshellarg($CFG->pathtogs); 443 $imageres = \escapeshellarg(100); 444 $imagefilearg = \escapeshellarg($imagefile); 445 $filename = \escapeshellarg($this->filename); 446 $pagenoinc = \escapeshellarg($pageno + 1); 447 $command = "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$pagenoinc -dLastPage=$pagenoinc ". 448 "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename"; 449 450 $output = null; 451 $result = exec($command, $output); 452 if (!file_exists($imagefile)) { 453 $fullerror = '<pre>'.get_string('command', 'assignfeedback_editpdf')."\n"; 454 $fullerror .= $command . "\n\n"; 455 $fullerror .= get_string('result', 'assignfeedback_editpdf')."\n"; 456 $fullerror .= htmlspecialchars($result) . "\n\n"; 457 $fullerror .= get_string('output', 'assignfeedback_editpdf')."\n"; 458 $fullerror .= htmlspecialchars(implode("\n",$output)) . '</pre>'; 459 throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror); 460 } 461 } 462 463 return 'image_page'.$pageno.'.png'; 464 } 465 466 /** 467 * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it 468 * @param stored_file $file 469 * @return string path to copy or converted pdf (false == fail) 470 */ 471 public static function ensure_pdf_compatible(\stored_file $file) { 472 global $CFG; 473 474 $temparea = \make_temp_directory('assignfeedback_editpdf'); 475 $hash = $file->get_contenthash(); // Use the contenthash to make sure the temp files have unique names. 476 $tempsrc = $temparea . "/src-$hash.pdf"; 477 $tempdst = $temparea . "/dst-$hash.pdf"; 478 $file->copy_content_to($tempsrc); // Copy the file. 479 480 $pdf = new pdf(); 481 $pagecount = 0; 482 try { 483 $pagecount = $pdf->load_pdf($tempsrc); 484 } catch (\Exception $e) { 485 // PDF was not valid - try running it through ghostscript to clean it up. 486 $pagecount = 0; 487 } 488 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 489 490 if ($pagecount > 0) { 491 // Page is valid and can be read by tcpdf. 492 return $tempsrc; 493 } 494 495 $gsexec = \escapeshellarg($CFG->pathtogs); 496 $tempdstarg = \escapeshellarg($tempdst); 497 $tempsrcarg = \escapeshellarg($tempsrc); 498 $command = "$gsexec -q -sDEVICE=pdfwrite -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg"; 499 exec($command); 500 @unlink($tempsrc); 501 if (!file_exists($tempdst)) { 502 // Something has gone wrong in the conversion. 503 return false; 504 } 505 506 $pdf = new pdf(); 507 $pagecount = 0; 508 try { 509 $pagecount = $pdf->load_pdf($tempdst); 510 } catch (\Exception $e) { 511 // PDF was not valid - try running it through ghostscript to clean it up. 512 $pagecount = 0; 513 } 514 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 515 516 if ($pagecount <= 0) { 517 @unlink($tempdst); 518 // Could not parse the converted pdf. 519 return false; 520 } 521 522 return $tempdst; 523 } 524 525 /** 526 * Test that the configured path to ghostscript is correct and working. 527 * @param bool $generateimage - If true - a test image will be generated to verify the install. 528 * @return bool 529 */ 530 public static function test_gs_path($generateimage = true) { 531 global $CFG; 532 533 $ret = (object)array( 534 'status' => self::GSPATH_OK, 535 'message' => null, 536 ); 537 $gspath = $CFG->pathtogs; 538 if (empty($gspath)) { 539 $ret->status = self::GSPATH_EMPTY; 540 return $ret; 541 } 542 if (!file_exists($gspath)) { 543 $ret->status = self::GSPATH_DOESNOTEXIST; 544 return $ret; 545 } 546 if (is_dir($gspath)) { 547 $ret->status = self::GSPATH_ISDIR; 548 return $ret; 549 } 550 if (!is_executable($gspath)) { 551 $ret->status = self::GSPATH_NOTEXECUTABLE; 552 return $ret; 553 } 554 555 if (!$generateimage) { 556 return $ret; 557 } 558 559 $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf'; 560 if (!file_exists($testfile)) { 561 $ret->status = self::GSPATH_NOTESTFILE; 562 return $ret; 563 } 564 565 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); 566 @unlink($testimagefolder.'/image_page0.png'); // Delete any previous test images. 567 568 $pdf = new pdf(); 569 $pdf->set_pdf($testfile); 570 $pdf->set_image_folder($testimagefolder); 571 try { 572 $pdf->get_image(0); 573 } catch (\moodle_exception $e) { 574 $ret->status = self::GSPATH_ERROR; 575 $ret->message = $e->getMessage(); 576 } 577 $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. 578 579 return $ret; 580 } 581 582 /** 583 * If the test image has been generated correctly - send it direct to the browser. 584 */ 585 public static function send_test_image() { 586 global $CFG; 587 header('Content-type: image/png'); 588 require_once($CFG->libdir.'/filelib.php'); 589 590 $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); 591 $testimage = $testimagefolder.'/image_page0.png'; 592 send_file($testimage, basename($testimage), 0); 593 die(); 594 } 595 596 } 597
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |