source: branches/MootoolsFileManager-Update/plugins/MootoolsFileManager/mootools-filemanager/Assets/Connector/Image.class.php @ 1300

Last change on this file since 1300 was 1300, checked in by gogo, 8 years ago

Update the MootoolsFileManager? to the latest cpojer with some modifications.
Add a demo for the MFM examples/mootools-file-manager.php
Change the default config for ImageManager? and ExtendedFileManager? for added security.

File size: 15.3 KB
Line 
1<?php
2/**
3 * Image - Provides an Interface to the GD-Library for image manipulation
4 *
5 *
6 * @license MIT-style License
7 * @author Christoph Pojer <christoph.pojer@gmail.com>
8 * @author Additions: Fabian Vogelsteller <fabian.vogelsteller@gmail.com>
9 *
10 * @link http://www.bin-co.com/php/scripts/classes/gd_image/ Based on work by "Binny V A"
11 *
12 * @version 1.11
13 * Changlog<br>
14 *    - 1.11 fixed $ratio in resize when both values are given
15 *    - 1.1 add real resizing, with comparison of ratio
16 *    - 1.01 small fixes in process method and add set memory limit to a higher value
17 */
18
19
20
21class Image {
22        /**
23         * The path to the image file
24         *
25         * @var string
26         */
27        private $file;
28        /**
29         * The image resource
30         *
31         * @var resource
32         */
33        private $image;
34        /**
35         * Metadata regarding the image
36         *
37         * @var array
38         */
39        private $meta;
40
41        /**
42         * @param string $file The path to the image file
43         */
44        public function __construct($file){
45
46                $finfo = self::guestimateRequiredMemorySpace($file);
47                $file = $finfo['path'];
48
49                // is it a valid file existing on disk?
50                if (!isset($finfo['imageinfo']))
51                        throw new Exception('no_imageinfo');
52
53                // only set the new memory limit of 64MB when the configured one is smaller:
54                if ($finfo['memory_limit'] < 64 * 1024 * 1024)
55                {
56                        ini_set('memory_limit', '64M'); //  handle large images
57                }
58
59                $this->file = $file;
60                $img = $finfo['imageinfo'];
61
62                // and will it fit in available memory if we go and load the bugger?
63                if (!$finfo['will_fit'])
64                        throw new Exception('img_will_not_fit:' . round(($finfo['usage_min_advised'] + 9.9E5) / 1E6) . ' MByte');
65
66                $explarr = explode('/', $img['mime']); // make sure the end() call doesn't throw an error next in E_STRICT mode:
67                $ext_from_mime = end($explarr);
68                $this->meta = array(
69                        'width' => $img[0],
70                        'height' => $img[1],
71                        'mime' => $img['mime'],
72                        'ext' => $ext_from_mime,
73                );
74
75                if($this->meta['ext']=='jpg')
76                        $this->meta['ext'] = 'jpeg';
77                if(!in_array($this->meta['ext'], array('gif', 'png', 'jpeg')))
78                        throw new Exception('unsupported_imgfmt:' . $this->meta['ext']);
79
80                if(in_array($this->meta['ext'], array('gif', 'png'))){
81                        $this->image = $this->create();
82
83                        $fn = 'imagecreatefrom'.$this->meta['ext'];
84                        $original = @$fn($file);
85                        if (!$original) throw new Exception('imagecreate_failed');
86
87                        if (!@imagecopyresampled($this->image, $original, 0, 0, 0, 0, $this->meta['width'], $this->meta['height'], $this->meta['width'], $this->meta['height']))
88                                throw new Exception('cvt2truecolor_failed:' . $this->meta['width'] . ' x ' . $this->meta['height']);
89                        imagedestroy($original);
90                        unset($original);
91                } else {
92                        $this->image = @imagecreatefromjpeg($file);
93                        if (!$this->image) throw new Exception('imagecreate_failed');
94                }
95        }
96
97        public function __destruct(){
98                if(!empty($this->image)) imagedestroy($this->image);
99                unset($this->image);
100        }
101
102        /**
103         * Guestimates how much RAM memory must be available to be able to process the given image file.
104         *
105         * @return an array with key,value pairs: 'needed' specifies the guestimated minimum amount of free
106         *         memory required for the given file, 'memory_limit' is an integer value representing the total
107         *         amount of bytes reserved for PHP script, while 'will_fit' is a boolean which indicates whether
108         *         the given image file could potentially be loaded and processed without causing fatal out-of-memory
109         *         errors.
110         *         The directory separator and path-corrected filespec is returned in the 'path' value.
111         *
112         * @note The given image file must exist on disk; if it does not, 'needed' and 'will_fit' keys will not
113         *       be present in the returned array.
114         */
115        public static function guestimateRequiredMemorySpace($file)
116        {
117                $val = trim(ini_get('memory_limit'));
118                $last = strtoupper(substr($val, -1, 1));
119                $val = floatval($val); // discards the KMG postfix, allow $val to carry values beyond 2..4G
120                switch($last)
121                {
122                // The 'G' modifier is available since PHP 5.1.0
123                case 'G':
124                        $val *= 1024.0;
125                case 'M':
126                        $val *= 1024.0;
127                case 'K':
128                        $val *= 1024.0;
129                        break;
130                }
131                $limit = $val;
132
133                $in_use = (function_exists('memory_get_usage') ? memory_get_usage() : 1000000 /* take a wild guess, er, excuse me, 'apply a heuristic' */ );
134
135
136                $file = str_replace('\\','/',$file);
137                $file = preg_replace('#/+#','/',$file);
138                $file = str_replace($_SERVER['DOCUMENT_ROOT'],'',$file);
139                $file = $_SERVER['DOCUMENT_ROOT'].$file;
140                $file = str_replace('\\','/',$file);
141                $file = preg_replace('#/+#','/',$file);
142                $file = realpath($file);
143                $file = str_replace('\\','/',$file);
144
145                $rv = array(
146                        'memory_limit' => $limit,
147                        'mem_in_use' => $in_use,
148                        'path' => $file
149                        );
150
151                if(file_exists($file))
152                {
153                        $raw_size = @filesize($file);
154                        $rv['filesize'] = $raw_size;
155
156                        $img = @getimagesize($file, $info_ex);
157                        if ($img)
158                        {
159                                $width = $img[0];
160                                $height = $img[1];
161                                $rv['imageinfo'] = $img;
162                                $rv['imageinfo_extra'] = $info_ex;
163
164                                // assume RGBA8, i.e. 4 bytes per pixel
165                                // ... having had a good look with memory_get_usage() and memory_get_peak_usage(), it turns out we need
166                                // a 'fudge factor' a.k.a. heuristic as the '4 bytes per pixel' estimate is off by quite a bit (if we have
167                                // to believe the numbers following a single GD image load operation):
168                                $needed = 4.0 * $width * $height;
169                                $needed *= 34.0 / 27.0;
170                                $rv['needed'] = $needed;
171
172                                // since many operations require a source and destination buffer, that'll be 2 of them buffers, thank you very much:
173                                // ... however, having had a good look with memory_get_usage() and memory_get_peak_usage(), it turns out
174                                // we need about triple!
175                                $will_eat = $needed * 2.8;
176                                // ^^^ factor = 2.8 : for very large images the estimation error is now ~ +1..8% too pessimistic. Soit!
177                                //     (tested with PNG images which consumed up to 475MB RAM to have their thumbnail created. This took a few
178                                //      seconds per image, so you might ask yourself if being able to serve such memory megalodons would be,
179                                //      ah, desirable, when considered from this perspective.)
180
181                                // and 'worst case' (ahem) we've got the file itself loaded in memory as well (on initial load and later save):
182                                // ... but this is more than covered by the 'triple charge' already, so we ditch this one from the heuristics.
183                                if (0) $will_eat += $raw_size;
184                                $rv['usage_guestimate'] = $will_eat;
185
186                                // now we know what we about need for this bugger, see if we got enough:
187                                $does_fit = ($limit - $in_use > $will_eat);
188                                $rv['usage_min_advised'] = $will_eat + $in_use;
189                                $rv['will_fit'] = $does_fit;
190                        }
191                        else
192                        {
193                                // else: this is not a valid image file!
194                                $rv['not_an_image_file'] = true;
195                        }
196                }
197                return $rv;
198        }
199
200        /**
201         * Returns the size of the image
202         *
203         * @return array
204         */
205        public function getSize(){
206                return array(
207                        'width' => $this->meta['width'],
208                        'height' => $this->meta['height'],
209                );
210        }
211
212        /**
213         * Creates a new, empty image with the desired size
214         *
215         * @param int $x
216         * @param int $y
217         * @param string $ext
218         * @return resource
219         */
220        private function create($x = null, $y = null, $ext = null){
221                if(!$x) $x = $this->meta['width'];
222                if(!$y) $y = $this->meta['height'];
223
224                $image = @imagecreatetruecolor($x, $y);
225                if (!$image) throw new Exception('imagecreatetruecolor_failed');
226                if(!$ext) $ext = $this->meta['ext'];
227                if($ext=='png'){
228                        if (!@imagealphablending($image, false))
229                                throw new Exception('imagealphablending_failed');
230                        $alpha = @imagecolorallocatealpha($image, 0, 0, 0, 127);
231                        if (!$alpha) throw new Exception('imageallocalpha50pctgrey_failed');
232                        imagefilledrectangle($image, 0, 0, $x, $y, $alpha);
233                }
234
235                return $image;
236        }
237
238        /**
239         * Replaces the image resource with the given parameter
240         *
241         * @param resource $new
242         */
243        private function set($new){
244          if(!empty($this->image)) imagedestroy($this->image);
245                $this->image = $new;
246
247                $this->meta['width'] = imagesx($this->image);
248                $this->meta['height'] = imagesy($this->image);
249        }
250
251        /**
252         * Returns the path to the image file
253         *
254         * @return string
255         */
256        public function getImagePath(){
257                return $this->file;
258        }
259
260        /**
261         * Returns the resource of the image file
262         *
263         * @return resource
264         */
265        public function getResource(){
266                return $this->image;
267        }
268
269        /**
270         * Rotates the image by the given angle
271         *
272         * @param int $angle
273         * @param array $bgcolor An indexed array with red/green/blue/alpha values
274         * @return Image
275         */
276        public function rotate($angle, $bgcolor = null){
277                if(empty($this->image) || !$angle || $angle>=360) return $this;
278
279                $alpha = (is_array($bgcolor) ? @imagecolorallocatealpha($this->image, $bgcolor[0], $bgcolor[1], $bgcolor[2], !empty($bgcolor[3]) ? $bgcolor[3] : null) : $bgcolor);
280                if (!$alpha) throw new Exception('imagecolorallocatealpha_failed');
281                $img = @imagerotate($this->image, $angle, $alpha);
282                if (!$img) throw new Exception('imagerotate_failed');
283                $this->set($img);
284                unset($img);
285
286                return $this;
287        }
288
289        /**
290         * Resizes the image to the given size, automatically calculates
291         * the new ratio if parameter {@link $ratio} is set to true
292         *
293         * @param int $x the maximum width after resizing has been done
294         * @param int $y the maximum height after resizing has been done
295         * @param bool $ratio set to FALSE if the image ratio is solely to be determined
296         *                    by the $x and $y parameter values; when TRUE (the default)
297         *                    the resize operation will keep the image aspect ratio intact
298         * @param bool $resizeWhenSmaller if FALSE the images will not be resized when
299         *                    already smaller, if TRUE the images will always be resized
300         * @return resource Image resource on success; throws an exception on failure
301         */
302        public function resize($x = null, $y = null, $ratio = true, $resizeWhenSmaller = true){
303                if(empty($this->image) || (empty($x) && empty($y))) return false;
304
305                $xStart = $x;
306        $yStart = $y;
307        $ratioX = $this->meta['width'] / $this->meta['height'];
308        $ratioY = $this->meta['height'] / $this->meta['width'];
309        //echo 'ALLOWED: <br>'.$xStart.'x'."<br>".$yStart.'y'."<br>---------------<br>";
310        // ->> keep the RATIO
311        if($ratio) {
312          //echo 'BEGINN: <br>'.$this->meta['width'].'x'."<br>".$this->meta['height'].'y'."<br><br>";
313                // -> align to WIDTH
314                if(!empty($x) && ($x < $this->meta['width'] || $resizeWhenSmaller))
315                  $y = $x / $ratioX;
316                // -> align to HEIGHT
317                elseif(!empty($y) && ($y < $this->meta['height'] || $resizeWhenSmaller))
318                  $x = $y / $ratioY;
319                else {
320                  $y = $this->meta['height'];
321                  $x = $this->meta['width'];
322                }
323          //echo 'BET: <br>'.$x.'x'."<br>".$y.'y'."<br><br>";
324          // ->> align to WIDTH AND HEIGHT
325          if((!empty($yStart) && $y > $yStart) || (!empty($xStart) && $x > $xStart)) {
326                if($y > $yStart) {
327                  $y = $yStart;
328                  $x = $y / $ratioY;
329                } elseif($x > $xStart) {
330                  $x = $xStart;
331                  $y = $x / $ratioX;
332                }
333          }
334        // ->> DONT keep the RATIO (but keep ration when only width OR height is set)
335        } else {
336          // RATIO X
337          if(!empty($y) && empty($x) && ($y < $this->meta['height'] || $resizeWhenSmaller))
338                $x = $y / $ratioX;
339          // RATIO Y
340          elseif(empty($y) && !empty($x) && ($x < $this->meta['width'] || $resizeWhenSmaller))
341                $y = $x / $ratioY;
342        }
343        $x = round($x);
344        $y = round($y);
345
346                //echo 'END: <br>'.$x.'x'."<br>".$y.'y'."<br><br>";
347
348                // speedup? only do the resize operation when it must happen:
349                if ($x != $this->meta['width'] || $y != $this->meta['height'])
350                {
351                        $new = $this->create($x, $y);
352                        if(@imagecopyresampled($new, $this->image, 0, 0, 0, 0, $x, $y, $this->meta['width'], $this->meta['height'])) {
353                                $this->set($new);
354                                unset($new);
355                        }
356                        else
357      {
358        throw new Exception('imagecopyresampled_failed');
359      }                 
360                }
361                return $this;
362        }
363
364        /**
365         * Crops the image. The values are given like margin/padding values in css
366         *
367         * <b>Example</b>
368         * <ul>
369         * <li>crop(10) - Crops by 10px on all sides</li>
370         * <li>crop(10, 5) - Crops by 10px on top and bottom and by 5px on left and right sides</li>
371         * <li>crop(10, 5, 5) - Crops by 10px on top and by 5px on left, right and bottom sides</li>
372         * <li>crop(10, 5, 3, 2) - Crops by 10px on top, 5px by right, 3px by bottom and 2px by left sides</li>
373         * </ul>
374         *
375         * @param int $top
376         * @param int $right
377         * @param int $bottom
378         * @param int $left
379         * @return Image
380         */
381        public function crop($top, $right = null, $bottom = null, $left = null){
382                if(empty($this->image)) return $this;
383
384                if(!is_numeric($right) && !is_numeric($bottom) && !is_numeric($left))
385                        $right = $bottom = $left = $top;
386
387                if(!is_numeric($bottom) && !is_numeric($left)){
388                        $bottom = $top;
389                        $left = $right;
390                }
391
392                if(!is_numeric($left))
393                        $left = $right;
394
395                $x = $this->meta['width']-$left-$right;
396                $y = $this->meta['height']-$top-$bottom;
397
398                if($x<0 || $y<0) return $this;
399
400                $new = $this->create($x, $y);
401                if (!@imagecopy($new, $this->image, 0, 0, $left, $top, $x, $y))
402                        throw new Exception('imagecopy_failed');
403
404                $this->set($new);
405                unset($new);
406
407                return $this;
408        }
409
410        /**
411         * Flips the image horizontally or vertically. To Flip both copy multiple single pixel strips around instead
412         * of just using ->rotate(180): no image distortion this way.
413         *
414         * @see Image::rotate()
415         * @param string $type Either horizontal or vertical
416         * @return Image
417         */
418        public function flip($type){
419                if(empty($this->image) || !in_array($type, array('horizontal', 'vertical'))) return $this;
420
421                $new = $this->create();
422
423                if($type=='horizontal')
424                {
425                        for($x=0;$x<$this->meta['width'];$x++)
426                        {
427                                if (!@imagecopy($new, $this->image, $this->meta['width']-$x-1, 0, $x, 0, 1, $this->meta['height']))
428                                        throw new Exception('imageflip_failed');
429                        }
430                }
431                elseif($type=='vertical')
432                {
433                        for($y=0;$y<$this->meta['height'];$y++)
434                        {
435                                if (!@imagecopy($new, $this->image, 0, $this->meta['height']-$y-1, 0, $y, $this->meta['width'], 1))
436                                        throw new Exception('imageflip_failed');
437                        }
438                }
439
440                $this->set($new);
441                unset($new);
442
443                return $this;
444        }
445
446        /**
447         * Stores the image in the desired directory or overwrite the old one
448         *
449         * @param string $ext
450         * @param string $file
451         * @param int $quality the amount of lossy compression to apply to the saved file
452         * @return Image object
453         */
454        public function process($ext = null, $file = null, $quality = 100){
455                if(empty($this->image)) return $this;
456
457                if(!$ext) $ext = $this->meta['ext'];
458                if($ext=='jpg') $ext = 'jpeg';
459                if($ext=='png') imagesavealpha($this->image, true);
460
461                if($file == null)
462                  $file = $this->file;
463                if(!$file) throw new Exception('process_nofile');
464
465                $fn = 'image'.$ext;
466                if($ext == 'jpeg')
467                  $rv = @$fn($this->image, $file, $quality);
468                elseif($ext == 'png')
469                  $rv = @$fn($this->image, $file, 9); // PNG is lossless: always maximum compression!
470                else
471                  $rv = @$fn($this->image, $file);
472                if (!$rv)
473                  throw new Exception($fn . '_failed');
474
475                // If there is a new filename change the internal name too
476                $this->file = $file;
477
478                return $this;
479        }
480
481        /**
482         * Saves the image to the given path
483         *
484         * @param string $file Leave empty to replace the original file
485         * @return Image
486         */
487        public function save($file = null){
488                if(empty($this->image)) return $this;
489
490                if(!$file) $file = $this->file;
491
492                $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
493                if(!$ext){
494                        $file .= '.'.$this->meta['ext'];
495                        $ext = $this->meta['ext'];
496                }
497
498                if($ext=='jpg') $ext = 'jpeg';
499
500                if(!in_array($ext, array('png', 'jpeg', 'gif')))
501                        return $this;
502
503                return $this->process($ext, $file);
504        }
505
506        /**
507         * Outputs the manipulated image. Implicitly overwrites the old one on disk.
508         *
509         * @return Image
510         */
511        public function show(){
512                if(empty($this->image)) return $this;
513
514                header('Content-type: '.$this->meta['mime']);
515                return $this->process();
516        }
517}
Note: See TracBrowser for help on using the repository browser.