8d0cd5b9594d3832dd65f59d1fb295fa046b042a
[moodle.git] / mod / assign / feedback / editpdf / classes / pdf.php
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/>.
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  */
25 namespace assignfeedback_editpdf;
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->libdir.'/pdflib.php');
31 require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/fpdi.php');
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 {
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;
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     /** No errors */
68     const UNOCONVPATH_OK = 'ok';
69     /** Not set */
70     const UNOCONVPATH_EMPTY = 'empty';
71     /** Does not exist */
72     const UNOCONVPATH_DOESNOTEXIST = 'doesnotexist';
73     /** Is a dir */
74     const UNOCONVPATH_ISDIR = 'isdir';
75     /** Not executable */
76     const UNOCONVPATH_NOTEXECUTABLE = 'notexecutable';
77     /** Test file missing */
78     const UNOCONVPATH_NOTESTFILE = 'notestfile';
79     /** Version not supported */
80     const UNOCONVPATH_VERSIONNOTSUPPORTED = 'versionnotsupported';
81     /** Any other error */
82     const UNOCONVPATH_ERROR = 'error';
84     /** Min. width an annotation should have */
85     const MIN_ANNOTATION_WIDTH = 5;
86     /** Min. height an annotation should have */
87     const MIN_ANNOTATION_HEIGHT = 5;
89     /**
90      * Combine the given PDF files into a single PDF. Optionally add a coversheet and coversheet fields.
91      * @param string[] $pdflist  the filenames of the files to combine
92      * @param string $outfilename the filename to write to
93      * @return int the number of pages in the combined PDF
94      */
95     public function combine_pdfs($pdflist, $outfilename) {
97         raise_memory_limit(MEMORY_EXTRA);
98         $olddebug = error_reporting(0);
100         $this->setPageUnit('pt');
101         $this->setPrintHeader(false);
102         $this->setPrintFooter(false);
103         $this->scale = 72.0 / 100.0;
104         $this->SetFont('helvetica', '', 16.0 * $this->scale);
105         $this->SetTextColor(0, 0, 0);
107         $totalpagecount = 0;
109         foreach ($pdflist as $file) {
110             $pagecount = $this->setSourceFile($file);
111             $totalpagecount += $pagecount;
112             for ($i = 1; $i<=$pagecount; $i++) {
113                 $this->create_page_from_source($i);
114             }
115         }
117         $this->save_pdf($outfilename);
118         error_reporting($olddebug);
120         return $totalpagecount;
121     }
123     /**
124      * The number of the current page in the PDF being processed
125      * @return int
126      */
127     public function current_page() {
128         return $this->currentpage;
129     }
131     /**
132      * The total number of pages in the PDF being processed
133      * @return int
134      */
135     public function page_count() {
136         return $this->pagecount;
137     }
139     /**
140      * Load the specified PDF and set the initial output configuration
141      * Used when processing comments and outputting a new PDF
142      * @param string $filename the path to the PDF to load
143      * @return int the number of pages in the PDF
144      */
145     public function load_pdf($filename) {
146         raise_memory_limit(MEMORY_EXTRA);
147         $olddebug = error_reporting(0);
149         $this->setPageUnit('pt');
150         $this->scale = 72.0 / 100.0;
151         $this->SetFont('helvetica', '', 16.0 * $this->scale);
152         $this->SetFillColor(255, 255, 176);
153         $this->SetDrawColor(0, 0, 0);
154         $this->SetLineWidth(1.0 * $this->scale);
155         $this->SetTextColor(0, 0, 0);
156         $this->setPrintHeader(false);
157         $this->setPrintFooter(false);
158         $this->pagecount = $this->setSourceFile($filename);
159         $this->filename = $filename;
161         error_reporting($olddebug);
162         return $this->pagecount;
163     }
165     /**
166      * Sets the name of the PDF to process, but only loads the file if the
167      * pagecount is zero (in order to count the number of pages)
168      * Used when generating page images (but not a new PDF)
169      * @param string $filename the path to the PDF to process
170      * @param int $pagecount optional the number of pages in the PDF, if known
171      * @return int the number of pages in the PDF
172      */
173     public function set_pdf($filename, $pagecount = 0) {
174         if ($pagecount == 0) {
175             return $this->load_pdf($filename);
176         } else {
177             $this->filename = $filename;
178             $this->pagecount = $pagecount;
179             return $pagecount;
180         }
181     }
183     /**
184      * Copy the next page from the source file and set it as the current page
185      * @return bool true if successful
186      */
187     public function copy_page() {
188         if (!$this->filename) {
189             return false;
190         }
191         if ($this->currentpage>=$this->pagecount) {
192             return false;
193         }
194         $this->currentpage++;
195         $this->create_page_from_source($this->currentpage);
196         return true;
197     }
199     /**
200      * Create a page from a source PDF.
201      *
202      * @param int $pageno
203      */
204     protected function create_page_from_source($pageno) {
205         // Get the size (and deduce the orientation) of the next page.
206         $template = $this->importPage($pageno);
207         $size = $this->getTemplateSize($template);
208         $orientation = 'P';
209         if ($size['w'] > $size['h']) {
210             $orientation = 'L';
211         }
212         // Create a page of the required size / orientation.
213         $this->AddPage($orientation, array($size['w'], $size['h']));
214         // Prevent new page creation when comments are at the bottom of a page.
215         $this->setPageOrientation($orientation, false, 0);
216         // Fill in the page with the original contents from the student.
217         $this->useTemplate($template);
218     }
220     /**
221      * Copy all the remaining pages in the file
222      */
223     public function copy_remaining_pages() {
224         $morepages = true;
225         while ($morepages) {
226             $morepages = $this->copy_page();
227         }
228     }
230     /**
231      * Add a comment to the current page
232      * @param string $text the text of the comment
233      * @param int $x the x-coordinate of the comment (in pixels)
234      * @param int $y the y-coordinate of the comment (in pixels)
235      * @param int $width the width of the comment (in pixels)
236      * @param string $colour optional the background colour of the comment (red, yellow, green, blue, white, clear)
237      * @return bool true if successful (always)
238      */
239     public function add_comment($text, $x, $y, $width, $colour = 'yellow') {
240         if (!$this->filename) {
241             return false;
242         }
243         $this->SetDrawColor(51, 51, 51);
244         switch ($colour) {
245             case 'red':
246                 $this->SetFillColor(249, 181, 179);
247                 break;
248             case 'green':
249                 $this->SetFillColor(214, 234, 178);
250                 break;
251             case 'blue':
252                 $this->SetFillColor(203, 217, 237);
253                 break;
254             case 'white':
255                 $this->SetFillColor(255, 255, 255);
256                 break;
257             default: /* Yellow */
258                 $this->SetFillColor(255, 236, 174);
259                 break;
260         }
262         $x *= $this->scale;
263         $y *= $this->scale;
264         $width *= $this->scale;
265         $text = str_replace('&lt;', '<', $text);
266         $text = str_replace('&gt;', '>', $text);
267         // Draw the text with a border, but no background colour (using a background colour would cause the fill to
268         // appear behind any existing content on the page, hence the extra filled rectangle drawn below).
269         $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */
270         if ($colour != 'clear') {
271             $newy = $this->GetY();
272             // Now we know the final size of the comment, draw a rectangle with the background colour.
273             $this->Rect($x, $y, $width, $newy - $y, 'DF');
274             // Re-draw the text over the top of the background rectangle.
275             $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */
276         }
277         return true;
278     }
280     /**
281      * Add an annotation to the current page
282      * @param int $sx starting x-coordinate (in pixels)
283      * @param int $sy starting y-coordinate (in pixels)
284      * @param int $ex ending x-coordinate (in pixels)
285      * @param int $ey ending y-coordinate (in pixels)
286      * @param string $colour optional the colour of the annotation (red, yellow, green, blue, white, black)
287      * @param string $type optional the type of annotation (line, oval, rectangle, highlight, pen, stamp)
288      * @param int[]|string $path optional for 'pen' annotations this is an array of x and y coordinates for
289      *              the line, for 'stamp' annotations it is the name of the stamp file (without the path)
290      * @param string $imagefolder - Folder containing stamp images.
291      * @return bool true if successful (always)
292      */
293     public function add_annotation($sx, $sy, $ex, $ey, $colour = 'yellow', $type = 'line', $path, $imagefolder) {
294         global $CFG;
295         if (!$this->filename) {
296             return false;
297         }
298         switch ($colour) {
299             case 'yellow':
300                 $colourarray = array(255, 207, 53);
301                 break;
302             case 'green':
303                 $colourarray = array(153, 202, 62);
304                 break;
305             case 'blue':
306                 $colourarray = array(125, 159, 211);
307                 break;
308             case 'white':
309                 $colourarray = array(255, 255, 255);
310                 break;
311             case 'black':
312                 $colourarray = array(51, 51, 51);
313                 break;
314             default: /* Red */
315                 $colour = 'red';
316                 $colourarray = array(239, 69, 64);
317                 break;
318         }
319         $this->SetDrawColorArray($colourarray);
321         $sx *= $this->scale;
322         $sy *= $this->scale;
323         $ex *= $this->scale;
324         $ey *= $this->scale;
326         $this->SetLineWidth(3.0 * $this->scale);
327         switch ($type) {
328             case 'oval':
329                 $rx = abs($sx - $ex) / 2;
330                 $ry = abs($sy - $ey) / 2;
331                 $sx = min($sx, $ex) + $rx;
332                 $sy = min($sy, $ey) + $ry;
334                 // $rx and $ry should be >= min width and height
335                 if ($rx < self::MIN_ANNOTATION_WIDTH) {
336                     $rx = self::MIN_ANNOTATION_WIDTH;
337                 }
338                 if ($ry < self::MIN_ANNOTATION_HEIGHT) {
339                     $ry = self::MIN_ANNOTATION_HEIGHT;
340                 }
342                 $this->Ellipse($sx, $sy, $rx, $ry);
343                 break;
344             case 'rectangle':
345                 $w = abs($sx - $ex);
346                 $h = abs($sy - $ey);
347                 $sx = min($sx, $ex);
348                 $sy = min($sy, $ey);
350                 // Width or height should be >= min width and height
351                 if ($w < self::MIN_ANNOTATION_WIDTH) {
352                     $w = self::MIN_ANNOTATION_WIDTH;
353                 }
354                 if ($h < self::MIN_ANNOTATION_HEIGHT) {
355                     $h = self::MIN_ANNOTATION_HEIGHT;
356                 }
357                 $this->Rect($sx, $sy, $w, $h);
358                 break;
359             case 'highlight':
360                 $w = abs($sx - $ex);
361                 $h = 8.0 * $this->scale;
362                 $sx = min($sx, $ex);
363                 $sy = min($sy, $ey) + ($h * 0.5);
364                 $this->SetAlpha(0.5, 'Normal', 0.5, 'Normal');
365                 $this->SetLineWidth(8.0 * $this->scale);
367                 // width should be >= min width
368                 if ($w < self::MIN_ANNOTATION_WIDTH) {
369                     $w = self::MIN_ANNOTATION_WIDTH;
370                 }
372                 $this->Rect($sx, $sy, $w, $h);
373                 $this->SetAlpha(1.0, 'Normal', 1.0, 'Normal');
374                 break;
375             case 'pen':
376                 if ($path) {
377                     $scalepath = array();
378                     $points = preg_split('/[,:]/', $path);
379                     foreach ($points as $point) {
380                         $scalepath[] = intval($point) * $this->scale;
381                     }
383                     if (!empty($scalepath)) {
384                         $this->PolyLine($scalepath, 'S');
385                     }
386                 }
387                 break;
388             case 'stamp':
389                 $imgfile = $imagefolder . '/' . clean_filename($path);
390                 $w = abs($sx - $ex);
391                 $h = abs($sy - $ey);
392                 $sx = min($sx, $ex);
393                 $sy = min($sy, $ey);
395                 // Stamp is always more than 40px, so no need to check width/height.
396                 $this->Image($imgfile, $sx, $sy, $w, $h);
397                 break;
398             default: // Line.
399                 $this->Line($sx, $sy, $ex, $ey);
400                 break;
401         }
402         $this->SetDrawColor(0, 0, 0);
403         $this->SetLineWidth(1.0 * $this->scale);
405         return true;
406     }
408     /**
409      * Save the completed PDF to the given file
410      * @param string $filename the filename for the PDF (including the full path)
411      */
412     public function save_pdf($filename) {
413         $olddebug = error_reporting(0);
414         $this->Output($filename, 'F');
415         error_reporting($olddebug);
416     }
418     /**
419      * Set the path to the folder in which to generate page image files
420      * @param string $folder
421      */
422     public function set_image_folder($folder) {
423         $this->imagefolder = $folder;
424     }
426     /**
427      * Generate an image of the specified page in the PDF
428      * @param int $pageno the page to generate the image of
429      * @throws moodle_exception
430      * @throws coding_exception
431      * @return string the filename of the generated image
432      */
433     public function get_image($pageno) {
434         global $CFG;
436         if (!$this->filename) {
437             throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename');
438         }
440         if (!$this->imagefolder) {
441             throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder');
442         }
444         if (!is_dir($this->imagefolder)) {
445             throw new \coding_exception('The specified image output folder is not a valid folder');
446         }
448         $imagefile = $this->imagefolder.'/image_page' . $pageno . '.png';
449         $generate = true;
450         if (file_exists($imagefile)) {
451             if (filemtime($imagefile)>filemtime($this->filename)) {
452                 // Make sure the image is newer than the PDF file.
453                 $generate = false;
454             }
455         }
457         if ($generate) {
458             // Use ghostscript to generate an image of the specified page.
459             $gsexec = \escapeshellarg($CFG->pathtogs);
460             $imageres = \escapeshellarg(100);
461             $imagefilearg = \escapeshellarg($imagefile);
462             $filename = \escapeshellarg($this->filename);
463             $pagenoinc = \escapeshellarg($pageno + 1);
464             $command = "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$pagenoinc -dLastPage=$pagenoinc ".
465                 "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename";
467             $output = null;
468             $result = exec($command, $output);
469             if (!file_exists($imagefile)) {
470                 $fullerror = '<pre>'.get_string('command', 'assignfeedback_editpdf')."\n";
471                 $fullerror .= $command . "\n\n";
472                 $fullerror .= get_string('result', 'assignfeedback_editpdf')."\n";
473                 $fullerror .= htmlspecialchars($result) . "\n\n";
474                 $fullerror .= get_string('output', 'assignfeedback_editpdf')."\n";
475                 $fullerror .= htmlspecialchars(implode("\n",$output)) . '</pre>';
476                 throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror);
477             }
478         }
480         return 'image_page'.$pageno.'.png';
481     }
483     /**
484      * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it
485      * @param stored_file $file
486      * @return string path to copy or converted pdf (false == fail)
487      */
488     public static function ensure_pdf_compatible(\stored_file $file) {
489         global $CFG;
491         $temparea = \make_temp_directory('assignfeedback_editpdf');
492         $hash = $file->get_contenthash(); // Use the contenthash to make sure the temp files have unique names.
493         $tempsrc = $temparea . "/src-$hash.pdf";
494         $tempdst = $temparea . "/dst-$hash.pdf";
495         $file->copy_content_to($tempsrc); // Copy the file.
497         $pdf = new pdf();
498         $pagecount = 0;
499         try {
500             $pagecount = $pdf->load_pdf($tempsrc);
501         } catch (\Exception $e) {
502             // PDF was not valid - try running it through ghostscript to clean it up.
503             $pagecount = 0;
504         }
505         $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
507         if ($pagecount > 0) {
508             // Page is valid and can be read by tcpdf.
509             return $tempsrc;
510         }
512         $gsexec = \escapeshellarg($CFG->pathtogs);
513         $tempdstarg = \escapeshellarg($tempdst);
514         $tempsrcarg = \escapeshellarg($tempsrc);
515         $command = "$gsexec -q -sDEVICE=pdfwrite -dBATCH -dNOPAUSE -sOutputFile=$tempdstarg $tempsrcarg";
516         exec($command);
517         @unlink($tempsrc);
518         if (!file_exists($tempdst)) {
519             // Something has gone wrong in the conversion.
520             return false;
521         }
523         $pdf = new pdf();
524         $pagecount = 0;
525         try {
526             $pagecount = $pdf->load_pdf($tempdst);
527         } catch (\Exception $e) {
528             // PDF was not valid - try running it through ghostscript to clean it up.
529             $pagecount = 0;
530         }
531         $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
533         if ($pagecount <= 0) {
534             @unlink($tempdst);
535             // Could not parse the converted pdf.
536             return false;
537         }
539         return $tempdst;
540     }
542     /**
543      * Test that the configured path to ghostscript is correct and working.
544      * @param bool $generateimage - If true - a test image will be generated to verify the install.
545      * @return bool
546      */
547     public static function test_gs_path($generateimage = true) {
548         global $CFG;
550         $ret = (object)array(
551             'status' => self::GSPATH_OK,
552             'message' => null,
553         );
554         $gspath = $CFG->pathtogs;
555         if (empty($gspath)) {
556             $ret->status = self::GSPATH_EMPTY;
557             return $ret;
558         }
559         if (!file_exists($gspath)) {
560             $ret->status = self::GSPATH_DOESNOTEXIST;
561             return $ret;
562         }
563         if (is_dir($gspath)) {
564             $ret->status = self::GSPATH_ISDIR;
565             return $ret;
566         }
567         if (!is_executable($gspath)) {
568             $ret->status = self::GSPATH_NOTEXECUTABLE;
569             return $ret;
570         }
572         if (!$generateimage) {
573             return $ret;
574         }
576         $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf';
577         if (!file_exists($testfile)) {
578             $ret->status = self::GSPATH_NOTESTFILE;
579             return $ret;
580         }
582         $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
583         @unlink($testimagefolder.'/image_page0.png'); // Delete any previous test images.
585         $pdf = new pdf();
586         $pdf->set_pdf($testfile);
587         $pdf->set_image_folder($testimagefolder);
588         try {
589             $pdf->get_image(0);
590         } catch (\moodle_exception $e) {
591             $ret->status = self::GSPATH_ERROR;
592             $ret->message = $e->getMessage();
593         }
594         $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
596         return $ret;
597     }
599     /**
600      * If the test image has been generated correctly - send it direct to the browser.
601      */
602     public static function send_test_image() {
603         global $CFG;
604         header('Content-type: image/png');
605         require_once($CFG->libdir.'/filelib.php');
607         $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test');
608         $testimage = $testimagefolder.'/image_page0.png';
609         send_file($testimage, basename($testimage), 0);
610         die();
611     }
613     /**
614      * If the test pdf has been generated correctly and send it direct to the browser.
615      */
616     public static function send_test_pdf() {
617         global $CFG;
618         require_once($CFG->libdir . '/filelib.php');
620         $filerecord = array(
621             'contextid' => \context_system::instance()->id,
622             'component' => 'test',
623             'filearea' => 'assignfeedback_editpdf',
624             'itemid' => 0,
625             'filepath' => '/',
626             'filename' => 'unoconv_test.docx'
627         );
629         // Get the fixture doc file content and generate and stored_file object.
630         $fs = get_file_storage();
631         $fixturefile = $CFG->libdir . '/tests/fixtures/unoconv-source.docx';
632         $fixturedata = file_get_contents($fixturefile);
633         $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'],
634                 $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);
635         if (!$testdocx) {
636             $testdocx = $fs->create_file_from_string($filerecord, $fixturedata);
638         }
640         // Convert the doc file to pdf and send it direct to the browser.
641         $result = $fs->get_converted_document($testdocx, 'pdf');
642         readfile_accel($result, 'application/pdf', true);
643     }
645     /**
646      * Check if unoconv configured path is correct and working.
647      *
648      * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
649      */
650     public static function test_unoconv_path() {
651         global $CFG;
652         $unoconvpath = $CFG->pathtounoconv;
654         $ret = new \stdClass();
655         $ret->status = self::UNOCONVPATH_OK;
656         $ret->message = null;
658         if (empty($unoconvpath)) {
659             $ret->status = self::UNOCONVPATH_EMPTY;
660             return $ret;
661         }
662         if (!file_exists($unoconvpath)) {
663             $ret->status = self::UNOCONVPATH_DOESNOTEXIST;
664             return $ret;
665         }
666         if (is_dir($unoconvpath)) {
667             $ret->status = self::UNOCONVPATH_ISDIR;
668             return $ret;
669         }
670         if (!is_executable($unoconvpath)) {
671             $ret->status = self::UNOCONVPATH_NOTEXECUTABLE;
672             return $ret;
673         }
674         if (self::check_unoconv_version_support() === false) {
675             $ret->status = self::UNOCONVPATH_VERSIONNOTSUPPORTED;
676             return $ret;
677         }
679         return $ret;
680     }
682     /**
683      * Check if the installed version of unoconv is supported.
684      *
685      * @return bool true if the present version is supported, false otherwise.
686      */
687     public static function check_unoconv_version_support() {
688         global $CFG;
689         $unoconvbin = \escapeshellarg($CFG->pathtounoconv);
690         $command = "$unoconvbin --version";
691         exec($command, $output);
692         preg_match('/([0-9]+\.[0-9]+)/', $output[0], $matches);
693         $currentversion = (float)$matches[0];
694         $supportedversion = 0.7;
695         if ($currentversion < $supportedversion) {
696             return false;
697         }
699         return true;
700     }