source: trunk/lib/ImageThumb.inc.php @ 817

Last change on this file since 817 was 817, checked in by anonymous, 4 weeks ago

Minor fixes. Set default cache expiration to 900 seconds (15 minutes).

File size: 37.0 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
24 * ImageThumb.inc.php
25 *
26 * @author   Quinn Comendant <quinn@strangecode.com>
27 * @requires Netpbm <http://sourceforge.net/projects/netpbm/> and libjpeg or GD.
28 * @version  2.0
29 */
30
31// Image proportion options.
32define('IMAGETHUMB_FIT_WIDTH', 1);
33define('IMAGETHUMB_FIT_HEIGHT', 2);
34define('IMAGETHUMB_FIT_LARGER', 3);
35define('IMAGETHUMB_STRETCH', 4);
36define('IMAGETHUMB_NO_SCALE', 5);
37
38// Image resize options.
39define('IMAGETHUMB_METHOD_NETPBM', 6);
40define('IMAGETHUMB_METHOD_GD', 7);
41
42class ImageThumb
43{
44
45    // General object parameters.
46    protected $_params = array(
47        // The location for images to create thumbnails from.
48        'source_dir' => null,
49
50        // Existing files will be overwritten when there is a name conflict?
51        'allow_overwriting' => false,
52
53        // The file permissions of the uploaded files. Remember, files will be owned by the web server user.
54        'dest_file_perms' => 0600,
55
56        // Permissions of auto-created directories. Must be at least 0700 with owner=apache.
57        'dest_dir_perms' => 0700,
58
59        // Require file to have one of the following file name extensions.
60        'valid_file_extensions' => array('jpg', 'jpeg', 'gif', 'png'),
61
62        // Method to use for resizing. (IMAGETHUMB_METHOD_NETPBM or IMAGETHUMB_METHOD_GD)
63        'resize_method' => IMAGETHUMB_METHOD_NETPBM,
64
65        // Netpbm and libjpeg binary locations.
66        'anytopnm_binary' => '/usr/bin/anytopnm',
67        'pnmscale_binary' => '/usr/bin/pnmscale',
68        'cjpeg_binary' => '/usr/bin/cjpeg',
69
70        // Which messages do we pass to raiseMsg? Use one of the MSG_* constants or false to disable.
71        'display_messages' => MSG_ALL,
72    );
73
74    // Default image size specs.
75    protected $_default_image_specs = array(
76        // The destination for an image thumbnail size.
77        // Use initial / to specify absolute paths, leave off to specify a path relative to source_dir (eg: ../thumbs).
78        'dest_dir' => null,
79
80        // Destination file types. (IMG_JPG, IMG_PNG, IMG_GIF, IMG_WBMP)
81        'dest_file_type' => IMG_JPG,
82
83        // Destination file types. ('jpg', 'png', 'gif', 'wbmp')
84        'dest_file_extension' => 'jpg',
85
86        // Type of scaling to perform, and sizes used to calculate max dimensions.
87        'scaling_type' => IMAGETHUMB_FIT_LARGER,
88        'width' => null,
89        'height' => null,
90
91        // Percentage quality of image compression output 0-100.
92        'quality' => 65,
93
94        // Create progressive jpegs?
95        'progressive' => false,
96
97        // If using GD method, apply sharpen filter. Requires PHP > 5.1.
98        'sharpen' => true,
99
100        // Integers between 1-100, useful values are 65-85.
101        'sharpen_value' => 75,
102
103        // If source image is smaller than thumbnail, allow upscaling?
104        'allow_upscaling' => false,
105
106        // If thumb exists and filesize is smaller than this, do not overwrite the thumb.
107        'keep_filesize' => null,
108    );
109
110    // Final specifications for image sizes, set with setSpec().
111    protected $_image_specs = array();
112
113    /**
114     * Set (or overwrite existing) parameters by passing an array of new parameters.
115     *
116     * @access public
117     * @param  array    $params     Array of parameters (key => val pairs).
118     */
119    public function setParam($params)
120    {
121        $app =& App::getInstance();
122
123        if (isset($params) && is_array($params)) {
124
125            // Enforce valid source_dir parameter.
126            if (isset($params['source_dir'])) {
127                // Source must be directory.
128                if (!is_dir($params['source_dir'])) {
129                    $app->logMsg(sprintf('Attempting to auto-create source directory: %s', $params['source_dir']), LOG_NOTICE, __FILE__, __LINE__);
130                    if (version_compare(PHP_VERSION, '5.0.0', '>=')) {
131                        // Recursive.
132                        mkdir($params['source_dir'], isset($params['dest_dir_perms']) ? $params['dest_dir_perms'] : $this->getParam('dest_dir_perms'), true);
133                    } else {
134                        mkdir($params['source_dir'], isset($params['dest_dir_perms']) ? $params['dest_dir_perms'] : $this->getParam('dest_dir_perms'));
135                    }
136                    if (!is_dir($params['source_dir'])) {
137                        $app->logMsg(sprintf('Source directory invalid: %s', $params['source_dir']), LOG_ERR, __FILE__, __LINE__);
138                        trigger_error(sprintf('Source directory invalid: %s', $params['source_dir']), E_USER_ERROR);
139                    }
140                }
141                // Source must be readable.
142                if (!is_readable($params['source_dir'])) {
143                    $app->logMsg(sprintf('Source directory not readable: %s', $params['source_dir']), LOG_ERR, __FILE__, __LINE__);
144                    trigger_error(sprintf('Source directory not readable: %s', $params['source_dir']), E_USER_ERROR);
145                }
146            }
147
148            // Merge new parameters with old overriding only those passed.
149            $this->_params = array_merge($this->_params, $params);
150        } else {
151            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
152        }
153    }
154
155    /**
156     * Return the value of a parameter, if it exists.
157     *
158     * @access public
159     * @param string $param        Which parameter to return.
160     * @return mixed               Configured parameter value.
161     */
162    public function getParam($param)
163    {
164        $app =& App::getInstance();
165
166        if (array_key_exists($param, $this->_params)) {
167            return $this->_params[$param];
168        } else {
169            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
170            return null;
171        }
172    }
173
174    /**
175     * Set the specification of thumbnails.
176     *
177     * @access  public
178     * @param   array   $spec   The specifications for a size of output image.
179     * @param   int     $index  The position of the specification in the spec array
180     *                          Use to overwrite existing spec array values.
181     */
182    public function setSpec($spec, $index=null)
183    {
184        $app =& App::getInstance();
185
186        // A little sanity checking.
187        if (!isset($spec['dest_dir']) || '' == trim($spec['dest_dir'])) {
188            $app->logMsg('setSpec error: dest_dir not specified.', LOG_ERR, __FILE__, __LINE__);
189        } else {
190            $spec['dest_dir'] = trim($spec['dest_dir']);
191        }
192        if (isset($spec['dest_file_type'])) {
193            switch ($spec['dest_file_type']) {
194            case IMG_JPG :
195                if (imagetypes() & IMG_JPG == 0) {
196                    $app->logMsg(sprintf('IMG_JPG is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
197                }
198                $spec['dest_file_extension'] = 'jpg';
199                break;
200            case IMG_PNG :
201                if (imagetypes() & IMG_PNG == 0) {
202                    $app->logMsg(sprintf('IMG_PNG is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
203                }
204                $spec['dest_file_extension'] = 'png';
205                break;
206            case IMG_GIF :
207                if (imagetypes() & IMG_GIF == 0) {
208                    $app->logMsg(sprintf('IMG_GIF is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
209                }
210                $spec['dest_file_extension'] = 'gif';
211                break;
212            case IMG_WBMP :
213                if (imagetypes() & IMG_WBMP == 0) {
214                    $app->logMsg(sprintf('IMG_WBMP is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
215                }
216                $spec['dest_file_extension'] = 'wbmp';
217                break;
218            default :
219                $app->logMsg(sprintf('Invalid dest_file_type: %s', $spec['dest_file_type']), LOG_ERR, __FILE__, __LINE__);
220                break;
221            }
222        }
223        if (IMAGETHUMB_FIT_HEIGHT != $spec['scaling_type'] && (!isset($spec['width']) || !is_numeric($spec['width']))) {
224            $app->logMsg(sprintf('setSpec error: invalid width: %s', $spec['width']), LOG_ERR, __FILE__, __LINE__);
225        }
226        if (IMAGETHUMB_FIT_WIDTH != $spec['scaling_type'] && (!isset($spec['height']) || !is_numeric($spec['height']))) {
227            $app->logMsg(sprintf('setSpec error: invalid height: %s', $spec['height']), LOG_ERR, __FILE__, __LINE__);
228        }
229        if (isset($spec['quality']) && IMG_JPG != $spec['dest_file_type']) {
230            $app->logMsg('The "quality" specification is not used unless IMG_JPG is the dest_file_type.', LOG_NOTICE, __FILE__, __LINE__);
231        }
232        if (isset($spec['progressive']) && IMG_JPG != $spec['dest_file_type']) {
233            $app->logMsg('The "progressive" specification is not used unless IMG_JPG is the dest_file_type.', LOG_NOTICE, __FILE__, __LINE__);
234        }
235
236        // Add to _image_specs array.
237        if (isset($index) && isset($this->_image_specs[$index])) {
238            // Merge with existing spec if index is provided.
239            $final_spec = array_merge($this->_image_specs[$index], $spec);
240            $this->_image_specs[$index] = $final_spec;
241        } else {
242            // Merge with spec defaults.
243            $final_spec = array_merge($this->_default_image_specs, $spec);
244            $this->_image_specs[] = $final_spec;
245        }
246
247        return $final_spec;
248    }
249
250    /*
251    * Retrieve a value of a thumb specification.
252    *
253    * @access   public
254    * @param    string  $key        Key to return. See _default_image_specs above for a list.
255    * @param    int     $index      The index in the spec array of the value to retrieve. The first if not specified.
256    * @param    string  $dest_dir   Return the spec matching this dest_dir (if used, the provided $index is ignored).
257    * @return   mixed               Value of requested $key for the spec selected by $index or $dest_dir.
258    * @author   Quinn Comendant <quinn@strangecode.com>
259    * @version  1.0
260    * @since    08 May 2007 15:26:39
261    */
262    public function getSpec($key, $index=null, $dest_dir=null)
263    {
264        $index = isset($index) ? $index : 0;
265        if (isset($dest_dir)) {
266            $index = array_search($dest_dir, array_column($this->_image_specs, 'dest_dir'));
267        }
268        return $this->_image_specs[$index][$key];
269    }
270
271    /**
272     * Process an entire directory of images.
273     *
274     * @access  public
275     * @return  bool true on success, false on failure.
276     */
277    public function processAll($runtime_specs=null)
278    {
279        $app =& App::getInstance();
280
281        // Ensure we have a source.
282        if ('' == $this->getParam('source_dir')) {
283            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
284            return false;
285        }
286
287        // Get all files in source directory.
288        $dir_handle = opendir($this->getParam('source_dir'));
289        while ($dir_handle && ($file = readdir($dir_handle)) !== false) {
290            // If the file name does not start with a dot (. or .. or .htaccess).
291            if (!preg_match('/^\./', $file) && in_array(mb_strtolower(mb_substr($file, mb_strrpos($file, '.') + 1)), $this->getParam('valid_file_extensions'))) {
292                $files[] = $file;
293            }
294        }
295
296        // Process each found file.
297        if (is_array($files) && !empty($files)) {
298            $return_val = 0;
299            foreach ($files as $file_name) {
300                $return_val += (false === $this->processFile($file_name, $runtime_specs) ? 1 : 0);
301            }
302            $this->_raiseMsg(sprintf(_("Resized %s images."), sizeof($files)), MSG_SUCCESS, __FILE__, __LINE__);
303            return 0 === $return_val;
304        } else {
305            $app->logMsg(sprintf('No source images found in directory: %s', $this->getParam('source_dir')), LOG_NOTICE, __FILE__, __LINE__);
306            return false;
307        }
308    }
309
310    /**
311     * Generate thumbnails for the specified file.
312     *
313     * @access  public
314     * @param   string $file_name Name of file with extension.
315     * @param   array $runtime_specs Array of specifications that will override all configured specifications.
316     * @return  bool true on success, false on failure.
317     */
318    public function processFile($file_name, $runtime_specs=null)
319    {
320        $app =& App::getInstance();
321
322        // Source file determined by provided file_name.
323        $source_file = realpath(sprintf('%s/%s', $this->getParam('source_dir'), $file_name));
324
325        // Ensure we have a source.
326        if (sizeof($this->_image_specs) < 1) {
327            if (is_array($runtime_specs)) {
328                $this->setSpec($runtime_specs, 0);
329            } else {
330                $app->logMsg(sprintf('Image specifications not set before processing.'), LOG_ERR, __FILE__, __LINE__);
331                return false;
332            }
333        }
334
335        // Ensure we have a source.
336        if ('' == $this->getParam('source_dir')) {
337            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
338            return false;
339        }
340
341        // Confirm source image exists.
342        if (!file_exists($source_file)) {
343            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> was not found."), $file_name), MSG_ERR, __FILE__, __LINE__);
344            $app->logMsg(sprintf('Source image not found: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
345            return false;
346        }
347
348        // Confirm source image is readable.
349        if (!is_readable($source_file)) {
350            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> is not readable."), $file_name), MSG_ERR, __FILE__, __LINE__);
351            $app->logMsg(sprintf('Source image not readable: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
352            return false;
353        }
354
355        // Confirm source image contains data.
356        if (filesize($source_file) <= 0) {
357            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> is zero bytes."), $file_name), MSG_ERR, __FILE__, __LINE__);
358            $app->logMsg(sprintf('Source image is zero bytes: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
359            return false;
360        }
361
362        // Confirm source image has a valid file extension.
363        if (!$this->_validFileExtension($file_name)) {
364            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> not a valid type. It must have one of the following file name extensions: %s"), $file_name, join(', ', $this->getParam('valid_file_extensions'))), MSG_ERR, __FILE__, __LINE__);
365            $app->logMsg(sprintf('Image resizing failed: source image not of valid type: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
366            return false;
367        }
368
369        // Ensure destination directories are created. This will only be called once per page load.
370        if (!$this->_createDestDirs()) {
371            $app->logMsg('Image resizing failed: unable to create dest dirs.', LOG_WARNING, __FILE__, __LINE__);
372            return false;
373        }
374
375        // To keep this script running even if user tries to stop browser.
376        ignore_user_abort(true);
377        ini_set('max_execution_time', 300);
378        ini_set('max_input_time', 300);
379
380        // This remains zero until something goes wrong.
381        $return_val = 0;
382
383        foreach ($this->_image_specs as $index => $spec) {
384
385            if (is_array($runtime_specs)) {
386                // Override with runtime specs.
387                $spec = $this->setSpec($runtime_specs, $index);
388            }
389
390            // Destination filename uses the extension defined by dest_file_extension.
391            if ('/' == $spec['dest_dir'][0]) {
392                // Absolute path.
393                $dest_file = sprintf('%s/%s.%s', $spec['dest_dir'], mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $spec['dest_file_extension']);
394            } else {
395                // Relative path.
396                $dest_file = sprintf('%s/%s/%s.%s', $this->getParam('source_dir'), $spec['dest_dir'], mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $spec['dest_file_extension']);
397            }
398
399            // Ensure destination directory exists and is writable.
400            if (!is_dir(dirname($dest_file)) || !is_writable(dirname($dest_file))) {
401                $this->_createDestDirs($dest_file);
402                if (!is_dir(dirname($dest_file)) || !is_writable(dirname($dest_file))) {
403                    $app->logMsg(sprintf('Image resizing failed, dest_dir invalid: %s', dirname($dest_file)), LOG_ERR, __FILE__, __LINE__);
404                    $return_val++;
405                    continue;
406                }
407            }
408
409            // Skip existing thumbnails with file size below $spec['keep_filesize'].
410            if (isset($spec['keep_filesize']) && file_exists($dest_file)) {
411                $file_size = filesize($dest_file);
412                if (false !== $file_size && $file_size < $spec['keep_filesize']) {
413                    $app->logMsg(sprintf('Skipping thumbnail %s. File already exists and file size is less than %s bytes.', $spec['dest_dir'] . '/' . $file_name, $spec['keep_filesize']), LOG_INFO, __FILE__, __LINE__);
414                    continue;
415                }
416            }
417
418            // Determine if original file size is smaller than specified thumbnail size. Do not scale-up if $spec['allow_upscaling'] config is set to false.
419            $image_size = getimagesize($source_file);
420            if ($image_size['0'] <= $spec['width'] && $image_size['1'] <= $spec['height'] && !$spec['allow_upscaling']) {
421                $spec['scaling_type'] = IMAGETHUMB_NO_SCALE;
422                $app->logMsg(sprintf('Image %s smaller than specified %s thumbnail size. Keeping original size.', $file_name, basename($spec['dest_dir'])), LOG_INFO, __FILE__, __LINE__);
423            }
424
425            // DO IT! Based on available method.
426            if (IMAGETHUMB_METHOD_NETPBM === $this->getParam('resize_method') && file_exists($this->getParam('anytopnm_binary')) && file_exists($this->getParam('pnmscale_binary')) && file_exists($this->getParam('cjpeg_binary'))) {
427                // Resize using Netpbm binaries.
428                $app->logMsg(sprintf('Resizing with Netpbm: %s', $source_file), LOG_DEBUG, __FILE__, __LINE__);
429                $return_val += $this->_resizeWithNetpbm($source_file, $dest_file, $spec);
430            } else if (IMAGETHUMB_METHOD_GD === $this->getParam('resize_method') && extension_loaded('gd')) {
431                // Resize with GD.
432                $app->logMsg(sprintf('Resizing with GD: %s', $source_file), LOG_DEBUG, __FILE__, __LINE__);
433                $return_val += $this->_resizeWithGD($source_file, $dest_file, $spec);
434            } else {
435                $app->logMsg(sprintf('Image thumbnailing canceled. Neither Netpbm or GD is available.', null), LOG_ERR, __FILE__, __LINE__);
436                return false;
437            }
438        }
439
440        // If > 0, there was a problem thumb-nailing.
441        return 0 === $return_val;
442    }
443
444    /*
445    * Use the Netpbm and libjpg cjpeg tools to generate a rescaled compressed image.
446    * This is the preferred method over GD which has (supposedly) less quality.
447    *
448    * @access   private
449    * @param    string  $source_file    Full path to source image file.
450    * @param    string  $dest_file      Full path to destination image file.
451    * @param    array   $spec           Array of image size specifications.
452    * @return   int                     0 if no error, n > 0 if errors.
453    * @author   Quinn Comendant <quinn@strangecode.com>
454    * @version  1.0
455    * @since    19 May 2006 13:55:46
456    */
457    protected function _resizeWithNetpbm($source_file, $dest_file, $spec)
458    {
459        $app =& App::getInstance();
460
461        // Define pnmscale arguments.
462        switch ($spec['scaling_type']) {
463        case IMAGETHUMB_FIT_WIDTH :
464            $pnmscale_args = sprintf(' -width %s ', escapeshellarg($spec['width']));
465            break;
466        case IMAGETHUMB_FIT_HEIGHT :
467            $pnmscale_args = sprintf(' -height %s ', escapeshellarg($spec['height']));
468            break;
469        case IMAGETHUMB_FIT_LARGER :
470            $pnmscale_args = sprintf(' -xysize %s %s ', escapeshellarg($spec['width']), escapeshellarg($spec['height']));
471            break;
472        case IMAGETHUMB_STRETCH :
473            $pnmscale_args = sprintf(' -width %s -height %s ', escapeshellarg($spec['width']), escapeshellarg($spec['height']));
474            break;
475        case IMAGETHUMB_NO_SCALE :
476        default :
477            $pnmscale_args = ' 1 ';
478            break;
479        }
480
481        // Define cjpeg arguments.
482        $cjpeg_args = sprintf(' -optimize -quality %s ', escapeshellarg($spec['quality']));
483        $cjpeg_args .= (true === $spec['progressive']) ? ' -progressive ' : '';
484
485        // Format the command that creates the thumbnail.
486        $command = sprintf('%s %s | %s %s | %s %s > %s',
487            escapeshellcmd($this->getParam('anytopnm_binary')),
488            escapeshellcmd($source_file),
489            escapeshellcmd($this->getParam('pnmscale_binary')),
490            escapeshellcmd($pnmscale_args),
491            escapeshellcmd($this->getParam('cjpeg_binary')),
492            escapeshellcmd($cjpeg_args),
493            escapeshellcmd($dest_file)
494        );
495        $app->logMsg(sprintf('ImageThumb Netpbm command: %s', $command), LOG_DEBUG, __FILE__, __LINE__);
496
497        // Execute!
498        exec($command, $output, $return_val);
499
500        if (0 === $return_val) {
501            // Success!
502            // Make the thumbnail writable so the user can delete it over ftp without being 'apache'.
503            chmod($dest_file, $this->getParam('dest_file_perms'));
504            $app->logMsg(sprintf('Successfully resized image %s', $spec['dest_dir'] . '/' . basename($dest_file), $return_val), LOG_DEBUG, __FILE__, __LINE__);
505        } else {
506            // An error occurred.
507            $app->logMsg(sprintf('Image %s failed resizing with return value: %s%s', $spec['dest_dir'] . '/' . basename($dest_file), $return_val, empty($output) ? '' : ' (' . truncate(getDump($output, true), 128, 'end') . ')'), LOG_ERR, __FILE__, __LINE__);
508        }
509
510        // Return from the command will be > 0 if there was an error.
511        return $return_val;
512    }
513
514    /*
515    * Use PHP's built-in GD tools to generate a rescaled compressed image.
516    *
517    * @access   private
518    * @param    string  $source_file    Full path to source image file.
519    * @param    string  $dest_file      Full path to destination image file.
520    * @param    array   $spec           Array of image size specifications.
521    * @return   int                     0 if no error, n > 0 if errors.
522    * @author   Quinn Comendant <quinn@strangecode.com>
523    * @version  1.0
524    * @since    19 May 2006 15:46:02
525    */
526    protected function _resizeWithGD($source_file, $dest_file, $spec)
527    {
528        $app =& App::getInstance();
529
530        // Get original file dimensions and type.
531        list($source_image_width, $source_image_height, $source_image_type) = getimagesize($source_file);
532
533        // Define destination image dimensions.
534        switch ($spec['scaling_type']) {
535        case IMAGETHUMB_FIT_WIDTH :
536            $dest_image_width = $spec['width'];
537            $dest_image_height = $source_image_height * ($spec['width'] / $source_image_width);
538            break;
539        case IMAGETHUMB_FIT_HEIGHT :
540            $dest_image_height = $spec['height'];
541            $dest_image_width = $source_image_width * ($spec['height'] / $source_image_height);
542            break;
543        case IMAGETHUMB_FIT_LARGER :
544            if (($source_image_width * ($spec['height'] / $source_image_height)) <= $spec['width']) {
545                // Height is larger.
546                $dest_image_height = $spec['height'];
547                $dest_image_width = $source_image_width * ($spec['height'] / $source_image_height);
548            } else {
549                // Width is larger.
550                $dest_image_width = $spec['width'];
551                $dest_image_height = $source_image_height * ($spec['width'] / $source_image_width);
552            }
553            break;
554        case IMAGETHUMB_STRETCH :
555            $dest_image_width = $spec['width'];
556            $dest_image_height = $spec['height'];
557            break;
558        case IMAGETHUMB_NO_SCALE :
559        default :
560            $dest_image_width = $source_image_width;
561            $dest_image_height = $source_image_height;
562            break;
563        }
564
565        // Create source image data in memory.
566        switch ($source_image_type) {
567        case IMAGETYPE_JPEG : // = 2
568            $source_image_resource = imagecreatefromjpeg($source_file);
569            break;
570        case IMAGETYPE_PNG : // = 3
571            $source_image_resource = imagecreatefrompng($source_file);
572            break;
573        case IMAGETYPE_GIF : // = 1
574            $source_image_resource = imagecreatefromgif($source_file);
575            break;
576        case IMAGETYPE_WBMP : // = 15
577            $source_image_resource = imagecreatefromwbmp($source_file);
578        case IMAGETYPE_SWF : // = 4
579        case IMAGETYPE_PSD : // = 5
580        case IMAGETYPE_BMP : // = 6
581        case IMAGETYPE_TIFF_II : // = 7
582        case IMAGETYPE_TIFF_MM : // = 8
583        case IMAGETYPE_JPC : // = 9
584        case IMAGETYPE_JP2 : // = 10
585        case IMAGETYPE_JPX : // = 11
586        case IMAGETYPE_JB2 : // = 12
587        case IMAGETYPE_SWC : // = 13
588        case IMAGETYPE_IFF : // = 14
589        case IMAGETYPE_XBM : // = 16
590        default :
591            $app->logMsg(sprintf('Source image type %s not supported.', $source_image_type), LOG_WARNING, __FILE__, __LINE__);
592            return 1;
593        }
594        if (!$source_image_resource) {
595            $app->logMsg(sprintf('Error creating %s image in memory from %s', $source_image_type, $source_file), LOG_WARNING, __FILE__, __LINE__);
596            return 1;
597        }
598
599        // Create destination image data in memory.
600        $dest_image_resource = imagecreatetruecolor($dest_image_width, $dest_image_height);
601
602        // Resample!
603        if (!imagecopyresampled($dest_image_resource, $source_image_resource, 0, 0, 0, 0, $dest_image_width, $dest_image_height, $source_image_width, $source_image_height)) {
604            // Always cleanup images from memory.
605            imagedestroy($source_image_resource);
606            imagedestroy($dest_image_resource);
607            $app->logMsg(sprintf('Error resampling image %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
608            return 1;
609        }
610
611        // Sharpen image using a custom filter matrix.
612        if (version_compare(PHP_VERSION, '5.1.0', '>=') && true === $spec['sharpen'] && $spec['sharpen_value'] > 0) {
613            $sharpen_value = round((((48 - 10) / (100 - 1)) * (100 - $spec['sharpen_value'])) + 10); // TODO: WTF is this math?
614            imageconvolution($dest_image_resource, array(array(-1, -1, -1), array(-1, $sharpen_value, -1),array(-1, -1, -1)), ($sharpen_value - 8), 0);
615        }
616
617        // Save image.
618        $return_val = true;
619        switch ($spec['dest_file_type']) {
620        case IMG_JPG :
621            imageinterlace($dest_image_resource, (true == $spec['progressive'] ? 1 : 0));
622            $return_val = imagejpeg($dest_image_resource, $dest_file, $spec['quality']);
623            break;
624        case IMG_PNG :
625            $return_val = imagepng($dest_image_resource, $dest_file);
626            break;
627        case IMG_GIF :
628            $return_val = imagegif($dest_image_resource, $dest_file);
629            break;
630        case IMG_WBMP :
631            $return_val = imagewbmp($dest_image_resource, $dest_file);
632            break;
633        default :
634            $app->logMsg(sprintf('Destination image type %s not supported for image %s.', $spec['dest_file_type'], $dest_file), LOG_WARNING, __FILE__, __LINE__);
635            // Always cleanup images from memory.
636            imagedestroy($source_image_resource);
637            imagedestroy($dest_image_resource);
638            return 1;
639            break;
640        }
641
642        // Always cleanup images from memory.
643        imagedestroy($source_image_resource);
644        imagedestroy($dest_image_resource);
645
646        if ($return_val) {
647            // Success!
648            // Make the thumbnail writable so the user can delete it over ftp without being 'apache'.
649            chmod($dest_file, $this->getParam('dest_file_perms'));
650            $app->logMsg(sprintf('Successfully resized image: %s', $dest_file), LOG_DEBUG, __FILE__, __LINE__);
651            return 0;
652        } else {
653            // An error occurred.
654            $app->logMsg(sprintf('Failed resizing image: %s', $dest_file), LOG_ERR, __FILE__, __LINE__);
655            return 1;
656        }
657    }
658
659    /**
660     * Delete the thumbnails for the specified file name.
661     *
662     * @access  public
663     * @param   string  $file_name  The file name to delete, with extension.
664     * @param   bool    $use_glob   Set true to use glob to find the filename (using $file_name as a pattern)
665     * @return  bool true on success, false on failure.
666     */
667    public function deleteThumbs($file_name, $use_glob=false)
668    {
669        $app =& App::getInstance();
670
671        // Ensure we have a source.
672        if ('' == $this->getParam('source_dir')) {
673            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
674            return false;
675        }
676
677        $return_val = 0;
678        foreach ($this->_image_specs as $spec) {
679            // Get base directory depending on if the spec's dest_dir is an absolute path or not.
680            $dest_dir = '/' == $spec['dest_dir'][0] ? $spec['dest_dir'] : sprintf('%s/%s', $this->getParam('source_dir'), $spec['dest_dir']);
681            if ($use_glob) {
682                $dest_file = realpath(sprintf('%s/%s', $dest_dir, $this->_glob($file_name, $dest_dir)));
683            } else {
684                $dest_file = realpath(sprintf('%s/%s.%s', $dest_dir, mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $spec['dest_file_extension']));
685            }
686
687            if (file_exists($dest_file)) {
688                if (!unlink($dest_file)) {
689                    $return_val++;
690                    $app->logMsg(sprintf('Delete thumbs failed: %s', $dest_file), LOG_WARNING, __FILE__, __LINE__);
691                }
692            }
693        }
694        $app->logMsg(sprintf('Thumbnails deleted for file: %s', $dest_file), LOG_DEBUG, __FILE__, __LINE__);
695        return 0 === $return_val;
696    }
697
698    /**
699     * Delete the source image with the specified file name.
700     *
701     * @access  public
702     * @param   string $file_name The file name to delete, with extension.
703     * @return  bool true on success, false on failure.
704     */
705    public function deleteOriginal($file_name)
706    {
707        $app =& App::getInstance();
708
709        // Ensure we have a source.
710        if ('' == $this->getParam('source_dir')) {
711            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
712            return false;
713        }
714
715        // Ensure we have a source.
716        if ('' == $file_name) {
717            $app->logMsg(sprintf('Cannot delete, filename empty.'), LOG_WARNING, __FILE__, __LINE__);
718            return false;
719        }
720
721        $source_file = realpath(sprintf('%s/%s', $this->getParam('source_dir'), $file_name));
722        if (is_file($source_file) && unlink($source_file)) {
723            $app->logMsg(sprintf('Original file successfully deleted: %s', $file_name), LOG_INFO, __FILE__, __LINE__);
724            return true;
725        } else {
726            $app->logMsg(sprintf('Delete original failed: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
727            return false;
728        }
729    }
730
731    /**
732     * Returns true if file exists.
733     *
734     * @access  public
735     * @param   string $file_name The file name to test, with extension.
736     * @return  bool true on success, false on failure.
737     */
738    public function exists($file_name)
739    {
740        $app =& App::getInstance();
741
742        // Ensure we have a source.
743        if ('' == $this->getParam('source_dir')) {
744            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
745            return false;
746        }
747
748        $source_file = realpath(sprintf('%s/%s', $this->getParam('source_dir'), $file_name));
749        return file_exists($source_file);
750    }
751
752    /**
753     * Tests if extension of $file_name is in the array valid_file_extensions.
754     *
755     * @access  public
756     * @param   string  $file_name  A file name.
757     * @return  bool    True on success, false on failure.
758     */
759    protected function _validFileExtension($file_name)
760    {
761        preg_match('/.*?\.(\w+)$/i', $file_name, $ext);
762        return !empty($ext) && in_array(mb_strtolower($ext[1]), $this->getParam('valid_file_extensions'));
763    }
764
765    /**
766     * Make directory for each specified thumbnail size, if it doesn't exist.
767     *
768     * @access  public
769     * @return  bool true on success, false on failure.
770     */
771    protected function _createDestDirs($filename=null)
772    {
773        $app =& App::getInstance();
774
775        // Keep track of directories we've already created.
776        $dd_hash = md5(isset($filename) ? dirname($filename) : 'none');
777        static $already_checked = array();
778
779        $return_val = 0;
780
781        if (!isset($already_checked[$dd_hash])) {
782            // Ensure we have a source.
783            if ('' == $this->getParam('source_dir')) {
784                $app->logMsg(sprintf('Source directory not set before creating destination directories.'), LOG_ERR, __FILE__, __LINE__);
785                return false;
786            }
787
788            // Loop through specs and ensure all dirs are created.
789            foreach ($this->_image_specs as $spec) {
790                if (isset($filename)) {
791                    $dest_dir = dirname($filename);
792                } else {
793                    // Get base directory depending on if the spec's dest_dir is an absolute path or not.
794                    $dest_dir = '/' == $spec['dest_dir'][0] ? $spec['dest_dir'] : sprintf('%s/%s', $this->getParam('source_dir'), $spec['dest_dir']);
795                }
796                if (!file_exists($dest_dir)) {
797                    // Recursively create destination directory.
798                    $app->logMsg(sprintf('Creating dest dir: %s', $dest_dir), LOG_DEBUG, __FILE__, __LINE__);
799                    if (!file_exists($dest_dir) && !($ret = mkdir($dest_dir, $this->getParam('dest_dir_perms'), true))) {
800                        $return_val++;
801                        $app->logMsg(sprintf('mkdir failure: %s', $dest_dir), LOG_ERR, __FILE__, __LINE__);
802                    }
803                    if ($ret) {
804                        $app->logMsg(sprintf('mkdir success: %s', $dest_dir), LOG_DEBUG, __FILE__, __LINE__);
805                    }
806                } else {
807                    $app->logMsg(sprintf('Dest dir exists: %s', $dest_dir), LOG_DEBUG, __FILE__, __LINE__);
808                }
809            }
810        }
811
812        $already_checked[$dd_hash] = true;
813
814        // If > 0, there was a problem creating dest dirs.
815        return 0 === $return_val;
816    }
817
818    /**
819     * An alias for $app->raiseMsg that only sends messages configured by display_messages.
820     *
821     * @access public
822     *
823     * @param string $message The text description of the message.
824     * @param int    $type    The type of message: MSG_NOTICE,
825     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
826     * @param string $file    __FILE__.
827     * @param string $line    __LINE__.
828     */
829    protected function _raiseMsg($message, $type, $file, $line)
830    {
831        $app =& App::getInstance();
832
833        if ($this->getParam('display_messages') === true || (is_int($this->getParam('display_messages')) && $this->getParam('display_messages') & $type > 0)) {
834            $app->raiseMsg($message, $type, $file, $line);
835        }
836    }
837
838    /**
839     * Get filename by glob pattern. Searches a directory for an image that matches the
840     * specified glob pattern and returns the filename of the first file found.
841     *
842     * @access  public
843     * @param   string  $pattern    Pattern to match filename.
844     * @param   string  $directory  Pattern to match containing directory.
845     * @return  string filename on success, empty string on failure.
846     * @author  Quinn Comendant <quinn@strangecode.com>
847     * @since   15 Nov 2005 20:55:22
848     */
849    protected function _glob($pattern, $directory)
850    {
851        $file_list = glob(sprintf('%s/%s', $directory, $pattern));
852        if (isset($file_list[0])) {
853            return basename($file_list[0]);
854        } else {
855            return '';
856        }
857    }
858
859} // End of class.
Note: See TracBrowser for help on using the repository browser.