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

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

Updates to http://www.github.com/sleemanj/mootools-filemanager from GerHoblett?

http://www.github.com/GerHoblett/

Changes to said updates by gogo (sleemanj @ github)

Modifications to MootoolsFileManager? to work with those updates, some courtesy of GerHoblett?, some sleemanj

GerHoblett? provided a large diff which accomplished the goal in a quite different way. It has merit, however I have opted for a less-affecting path in so far as Xinha's "way" is concerned, namely, not splitting the config for a single plugin into several calls to backend config passing functions which seemed a little cumbersome.

Instead I take the option of using POST to send backend data around, at the minor expense of an extra round trip when displaying thumbnails (for each one). This could be reduced by checking for thumbnail existence and returning the thumbnail name directly in "onView" rather than the backend request to generate said thumbnail.

Still to do, is to make the preview pane thumbnail also work.


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