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

Last change on this file since 1 was 1, checked in by scdev, 19 years ago

Initial import.

File size: 17.7 KB
Line 
1<?php
2/**
3 * ImageThumb.inc.php
4 * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
5 */
6
7/**
8 * The ImageThumb class resizes images.
9 *
10 * @author   Quinn Comendant <quinn@strangecode.com>
11 * @requires Netpbm binaries from http://sourceforge.net/projects/netpbm/
12 * @version  1.1
13 */
14
15define('IMAGETHUMB_FIT_WIDTH', 1);
16define('IMAGETHUMB_FIT_HEIGHT', 2);
17define('IMAGETHUMB_FIT_LARGER', 3);
18define('IMAGETHUMB_STRETCH', 4);
19define('IMAGETHUMB_NO_SCALE', 5);
20
21class ImageThumb {
22   
23    // The location for images to create thumbnails from.
24    var $source_dir = null;
25   
26    // Specifications for thumbnail images.
27    var $spec; 
28   
29    // Array of acceptable file extensions (lowercase).
30    var $valid_file_extensions = array('jpg', 'jpeg', 'gif', 'png');
31   
32    // The uploaded files will be owned by user 'apache'. Set world-read/write
33    // if the website admin needs to read/delete these files. Must be at least 0400 with owner=apache.
34    var $dest_file_perms = 0644;
35   
36    // Must be at least 0700 with owner=apache.
37    var $dest_dir_perms = 0777;
38
39    // Executable binary locations.
40    var $anytopnm_binary = '/usr/bin/anytopnm';
41    var $pnmscale_binary = '/usr/bin/pnmscale';
42    var $cjpeg_binary = '/usr/bin/cjpeg';
43    var $_valid_binaries = true;
44   
45    // Display messages raised in this object?
46    var $display_messages = true;
47
48    /**
49     * Constructor.
50     *
51     * @access  public
52     */
53    function ImageThumb()
54    {
55        if (!file_exists($this->anytopnm_binary)) {
56            App::logMsg(sprintf('ImageThumb error: anytopnm binary %s not found.', $this->anytopnm_binary), LOG_ERR, __FILE__, __LINE__);
57            $this->_valid_binaries = false;
58        }
59        if (!file_exists($this->pnmscale_binary)) {
60            App::logMsg(sprintf('ImageThumb error: pnmscale binary %s not found.', $this->pnmscale_binary), LOG_ERR, __FILE__, __LINE__);
61            $this->_valid_binaries = false;
62        }
63        if (!file_exists($this->cjpeg_binary)) {
64            App::logMsg(sprintf('ImageThumb error: cjpeg binary %s not found.', $this->cjpeg_binary), LOG_ERR, __FILE__, __LINE__);
65            $this->_valid_binaries = false;
66        }
67    }
68
69    /**
70     * Set the directory of the source images.
71     *
72     * @access  public
73     * @param   string $source_dir The full directory path of the source images.
74     * @return  bool true on success, false on failure.
75     */
76    function setSourceDirectory($source_dir)
77    {
78       
79        // Set the source directory path, stripping any extra slashes if needed.
80        $this->source_dir = preg_replace('!/+$!', '', $source_dir);
81       
82        if (!is_dir($this->source_dir)) {
83            App::logMsg(sprintf('ImageThumb error: source directory not found: %s', $this->source_dir), LOG_ERR, __FILE__, __LINE__);
84            return false;
85        }
86        if (!is_readable($this->source_dir)) {
87            App::logMsg(sprintf('ImageThumb error: source directory not readable: %s', $this->source_dir), LOG_ERR, __FILE__, __LINE__);
88            return false;
89        }
90        return true;
91    }
92
93    /**
94     * Set the specification of thumbnails.
95     *
96     * @access  public
97     * @param   array $spec The specifications for each size of output image.
98     * @return  bool true on success, false on failure.
99     */
100    function setSpec($spec = array())
101    {
102        $dest_dir        = preg_replace('!/+$!', '', $spec['dest_dir']);
103        $width           = $spec['width'];
104        $height          = $spec['height'];
105        $scaling_type    = $spec['scaling_type'];
106        $quality         = isset($spec['quality']) ? $spec['quality'] : 75;
107        $progressive     = isset($spec['progressive']) ? $spec['progressive'] : false;
108        $allow_upscaling = isset($spec['allow_upscaling']) ? $spec['allow_upscaling'] : false;
109        $keep_filesize   = isset($spec['keep_filesize']) ? $spec['keep_filesize'] : null;
110   
111        // Define pnmscale arguments.
112        switch ($scaling_type) {
113        case IMAGETHUMB_FIT_WIDTH :
114            if (empty($width)) {
115                App::logMsg('ImageThumb error: width not specified for IMAGETHUMB_FIT_WIDTH.', LOG_ERR, __FILE__, __LINE__);
116                return false;
117            }
118            $pnmscale_args = sprintf(' -width %s ', escapeshellcmd($width));
119            break;
120        case IMAGETHUMB_FIT_HEIGHT :
121            if (empty($height)) {
122                App::logMsg('ImageThumb error: height not specified for IMAGETHUMB_FIT_HEIGHT.', LOG_ERR, __FILE__, __LINE__);
123                return false;
124            }
125            $pnmscale_args = sprintf(' -height %s ', escapeshellcmd($height));
126            break;
127        case IMAGETHUMB_FIT_LARGER :
128            if (empty($width) || empty($height)) {
129                App::logMsg('ImageThumb error: width or height not specified for IMAGETHUMB_FIT_LARGER.', LOG_ERR, __FILE__, __LINE__);
130                return false;
131            }
132            $pnmscale_args = sprintf(' -xysize %s %s ', escapeshellcmd($width), escapeshellcmd($height));
133            break;
134        case IMAGETHUMB_STRETCH :
135            if (empty($width) || empty($height)) {
136                App::logMsg('ImageThumb error: width or height not specified for IMAGETHUMB_STRETCH.', LOG_ERR, __FILE__, __LINE__);
137                return false;
138            }
139            $pnmscale_args = sprintf(' -width %s -height %s ', escapeshellcmd($width), escapeshellcmd($height));
140            break;
141        case IMAGETHUMB_NO_SCALE :
142        default :
143            $pnmscale_args = ' 1 ';
144            break;
145        }
146       
147        // Define cjpeg arguments.
148        $cjpeg_args = sprintf(' -optimize -quality %s ', escapeshellcmd($quality));
149        $cjpeg_args .= (true === $progressive) ? ' -progressive ' : '';
150       
151        $this->spec[] = array(
152            'dest_dir' => $dest_dir,
153            'width' => $width,
154            'height' => $height,
155            'scaling_type' => $scaling_type,
156            'quality' => $quality,
157            'progressive' => $progressive,
158            'pnmscale_args' => $pnmscale_args,
159            'cjpeg_args' => $cjpeg_args,
160            'allow_upscaling' => $allow_upscaling,
161            'keep_filesize' => $keep_filesize,
162        );
163    }
164
165    /**
166     * Make directory for each specified thumbnail size, if it doesn't exist.
167     *
168     * @access  public
169     * @return  bool true on success, false on failure.
170     */
171    function createDestDirs()
172    {
173        // Ensure we have a source.
174        if (!isset($this->source_dir)) {
175            App::logMsg(sprintf('Source directory not set before creating destination directories.'), LOG_ERR, __FILE__, __LINE__);
176            return false;
177        }
178        $return_val = 0;
179        foreach ($this->spec as $s) {
180            if (!is_dir($this->source_dir . '/' . $s['dest_dir'])) {
181                if (!mkdir($this->source_dir . '/' . $s['dest_dir'], $this->dest_dir_perms)) {
182                    $return_val += 1;
183                    App::logMsg(sprintf('mkdir failure: %s', $this->source_dir . '/' . $s['dest_dir']), LOG_ERR, __FILE__, __LINE__);
184                }
185            }
186        }
187               
188        // If > 0, there was a problem creating dest dirs.
189        return (0 == $return_val);
190    }
191
192    /**
193     * Generate thumbnails for the specified file.
194     *
195     * @access  public
196     * @param   string $file_name Name of file with extention.
197     * @return  bool true on success, false on failure.
198     */
199    function processFile($file_name)
200    {
201        // Ensure we have valid binaries.
202        if (!$this->_valid_binaries) {
203            return false;
204        }
205       
206        // Ensure we have a source.
207        if (!isset($this->source_dir)) {
208            App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
209            return false;
210        }
211       
212        // To keep this script running even if user tries to stop browser.
213        ignore_user_abort(true); 
214        if (!ini_get('safe_mode')) { 
215            set_time_limit(300); 
216        }
217       
218        // Confirm source image exists.
219        if (!file_exists($this->source_dir . '/' . $file_name)) {
220            $this->raiseMsg(sprintf(_("Image resizing failed: source image not found: <strong>%s</strong>"), $file_name), MSG_ERR, __FILE__, __LINE__);
221            App::logMsg(sprintf('Source image not found: %s', $file_name), LOG_ALERT, __FILE__, __LINE__);
222            return false;
223        }
224       
225        // Confirm source image is readable.
226        if (!is_readable($this->source_dir . '/' . $file_name)) {
227            $this->raiseMsg(sprintf(_("Image resizing failed: source image not readable: <strong>%s</strong>"), $file_name), MSG_ERR, __FILE__, __LINE__);
228            App::logMsg(sprintf('Source image not readable: %s', $file_name), LOG_ALERT, __FILE__, __LINE__);
229            return false;
230        }
231       
232        // Confirm source image contains data.
233        if (filesize($this->source_dir . '/' . $file_name) < 1) {
234            $this->raiseMsg(sprintf(_("Image resizing failed: source image corrupt: <strong>%s</strong>"), $file_name), MSG_ERR, __FILE__, __LINE__);
235            App::logMsg(sprintf('Source image is zero bytes: %s', $file_name), LOG_ALERT, __FILE__, __LINE__);
236            return false;
237        }
238       
239        // Confirm source image has a valid file extension.
240        if (!$this->validFileExtension($file_name)) {
241            $this->raiseMsg(sprintf(_("Image resizing failed: source image not of valid type: <strong>%s</strong>"), $file_name), MSG_ERR, __FILE__, __LINE__);
242            App::logMsg(sprintf('Image resizing failed: source image not of valid type: %s', $file_name), LOG_ERR, __FILE__, __LINE__);
243            return false;
244        }
245       
246        // Output file will be a jpg. Set file extension.
247        $file_name = substr($file_name, 0, strrpos($file_name, '.')) . '.jpg';
248
249        // This remains zero until something goes wrong.
250        $final_return_val = 0;
251       
252        foreach ($this->spec as $s) {
253       
254            // Skip existing thumbnails with file size below $s['keep_filesize'].
255            if (file_exists(realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name)) && isset($s['keep_filesize'])) {
256                $file_size = filesize(realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name));
257                if ($file_size && $file_size < $s['keep_filesize']) {
258                    App::logMsg(sprintf('Skipping thumbnail %s. File already exists and file size is less than %s bytes.', $s['dest_dir'] . '/' . $file_name, $s['keep_filesize']), LOG_DEBUG, __FILE__, __LINE__);
259                    continue;
260                }
261            }
262           
263            // Determine if original file size is smaller than specified thumbnail size. Do not scale-up if allow_upscaling config is set to false.
264            $image_size = getimagesize(realpath($this->source_dir . '/' . $file_name));
265            if ($image_size['0'] <= $s['width'] && $image_size['1'] <= $s['height'] && !$s['allow_upscaling']) {
266                $pnmscale_args = ' 1 ';
267                App::logMsg(sprintf('Image %s smaller than specified %s thumbnail size. Keeping original size.', $file_name, $s['dest_dir']), LOG_DEBUG, __FILE__, __LINE__);
268            } else {
269                $pnmscale_args = $s['pnmscale_args'];
270            }
271           
272            // Execute the command that creates the thumbnail.
273            $command = sprintf('%s %s/%s | %s %s | %s %s > %s/%s',
274                escapeshellcmd($this->anytopnm_binary),
275                escapeshellcmd($this->source_dir),
276                escapeshellcmd($file_name),
277                escapeshellcmd($this->pnmscale_binary),
278                escapeshellcmd($pnmscale_args),
279                escapeshellcmd($this->cjpeg_binary),
280                escapeshellcmd($s['cjpeg_args']), 
281                escapeshellcmd(realpath($this->source_dir . '/' . $s['dest_dir'])),
282                escapeshellcmd($file_name)
283            );
284            App::logMsg(sprintf('ImageThumb command: %s', $command), LOG_DEBUG, __FILE__, __LINE__);
285            exec($command, $output, $return_val);
286
287            if (0 == $return_val) {
288                // Make the thumbnail writable so the user can delete it over ftp without being 'apache'.
289                chmod(realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name), $this->dest_file_perms);
290                App::logMsg(sprintf('Successfully resized image %s', $s['dest_dir'] . '/' . $file_name, $return_val), LOG_DEBUG, __FILE__, __LINE__);
291            } else {
292                App::logMsg(sprintf('Image %s failed resizing with return value: %s%s', $s['dest_dir'] . '/' . $file_name, $return_val, empty($output) ? '' : ' (' . getDump($output) . ')'), LOG_ERR, __FILE__, __LINE__);
293            }
294           
295            // Return from the command will be > 0 if there was an error.
296            $final_return_val += $return_val;
297        }
298       
299        // If > 0, there was a problem thumbnailing.
300        return (0 == $final_return_val);
301    }
302
303    /**
304     * Process an entire directory of images.
305     *
306     * @access  public
307     * @return  bool true on success, false on failure.
308     */
309    function processAll()
310    {
311        // Ensure we have a source.
312        if (!isset($this->source_dir)) {
313            App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
314            return false;
315        }
316       
317        // Get all files in source directory.
318        $dir_handle = opendir($this->source_dir);
319        while ($dir_handle && ($file = readdir($dir_handle)) !== false) {
320            // If the file name does not start with a dot (. or .. or .htaccess).
321            if (!preg_match('/^\./', $file) && in_array(strtolower(substr($file, strrpos($file, '.') + 1)), $this->valid_file_extensions)) {
322                $files[] = $file;
323            }
324        }
325       
326        // Process each found file.
327        if (is_array($files) && !empty($files)) {
328            foreach ($files as $file_name) {
329                $this->processFile($file_name);
330            }
331            return sizeof($files);
332        } else {
333            App::logMsg(sprintf('No images found to thumbnail in directory %s.', $this->source_dir), LOG_NOTICE, __FILE__, __LINE__);
334            return 0;
335        }
336    }
337
338    /**
339     * Delete the thumbnails for the specified file name.
340     *
341     * @access  public
342     * @param   string $file_name The file name to delete, with extention.
343     * @return  bool true on success, false on failure.
344     */
345    function deleteThumbs($file_name)
346    {
347        // Ensure we have a source.
348        if (!isset($this->source_dir)) {
349            App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
350            return false;
351        }
352       
353        $ret = 0;
354        foreach ($this->spec as $s) {
355            $file_path_name = realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name);
356            if (file_exists($file_path_name)) {
357                if (!unlink($file_path_name)) {
358                    $ret++;
359                    App::logMsg(sprintf(_("Delete thumbs failed: %s"), $file_path_name), LOG_WARNING, __FILE__, __LINE__);
360                }
361            }
362        }
363        $this->raiseMsg(sprintf(_("The thumbnails for file <strong>%s</strong> have been deleted."), $file_name), MSG_SUCCESS, __FILE__, __LINE__);
364        return (0 == $ret);
365    }
366
367    /**
368     * Delete the source image with the specified file name.
369     *
370     * @access  public
371     * @param   string $file_name The file name to delete, with extention.
372     * @return  bool true on success, false on failure.
373     */
374    function deleteOriginal($file_name)
375    {
376        // Ensure we have a source.
377        if (!isset($this->source_dir)) {
378            App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
379            return false;
380        }
381       
382        $file_path_name = $this->source_dir . '/' . $file_name;
383        if (!unlink($file_path_name)) {
384            App::logMsg(sprintf(_("Delete original failed: %s"), $file_path_name), LOG_WARNING, __FILE__, __LINE__);
385            return false;
386        }
387        $this->raiseMsg(sprintf(_("The original file <strong>%s</strong> has been deleted."), $file_name), MSG_SUCCESS, __FILE__, __LINE__);
388        return true;
389    }
390   
391    /**
392     * Returns true if file exists.
393     *
394     * @access  public
395     * @param   string $file_name The file name to test, with extention.
396     * @return  bool true on success, false on failure.
397     */
398    function exists($file_name)
399    {
400        // Ensure we have a source.
401        if (!isset($this->source_dir)) {
402            App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
403            return false;
404        }
405       
406        return file_exists($this->source_dir . '/' . $file_name);
407    }
408   
409    /**
410     * Tests if extention of $file_name is in the array valid_file_extensions.
411     *
412     * @access  public
413     * @param   string  $file_name  A file name.
414     * @return  bool    True on success, false on failure.
415     */
416    function validFileExtension($file_name)
417    {
418        preg_match('/.*?\.(\w+)$/i', $file_name, $ext);
419        return in_array(strtolower($ext[1]), $this->valid_file_extensions);
420    }
421   
422    /**
423     * An alias for App::raiseMsg that only sends messages if display_messages is true.
424     *
425     * @access public
426     *
427     * @param string $message The text description of the message.
428     * @param int    $type    The type of message: MSG_NOTICE,
429     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
430     * @param string $file    __FILE__.
431     * @param string $line    __LINE__.
432     */
433    function raiseMsg($message, $type, $file, $line)
434    {
435        if ($this->display_messages) {
436            App::raiseMsg($message, $type, $file, $line);
437        }
438    }
439
440} // End of class.
441?>
Note: See TracBrowser for help on using the repository browser.