* @requires Netpbm binaries from http://sourceforge.net/projects/netpbm/ * @version 1.1 */ define('IMAGETHUMB_FIT_WIDTH', 1); define('IMAGETHUMB_FIT_HEIGHT', 2); define('IMAGETHUMB_FIT_LARGER', 3); define('IMAGETHUMB_STRETCH', 4); define('IMAGETHUMB_NO_SCALE', 5); class ImageThumb { // The location for images to create thumbnails from. var $source_dir = null; // Specifications for thumbnail images. var $spec; // Array of acceptable file extensions (lowercase). var $valid_file_extensions = array('jpg', 'jpeg', 'gif', 'png'); // The uploaded files will be owned by user 'apache'. Set world-read/write // if the website admin needs to read/delete these files. Must be at least 0400 with owner=apache. var $dest_file_perms = 0644; // Must be at least 0700 with owner=apache. var $dest_dir_perms = 0777; // Executable binary locations. var $anytopnm_binary = '/usr/bin/anytopnm'; var $pnmscale_binary = '/usr/bin/pnmscale'; var $cjpeg_binary = '/usr/bin/cjpeg'; var $_valid_binaries = true; // Display messages raised in this object? var $display_messages = true; /** * Constructor. * * @access public */ function ImageThumb() { if (!file_exists($this->anytopnm_binary)) { App::logMsg(sprintf('ImageThumb error: anytopnm binary %s not found.', $this->anytopnm_binary), LOG_ERR, __FILE__, __LINE__); $this->_valid_binaries = false; } if (!file_exists($this->pnmscale_binary)) { App::logMsg(sprintf('ImageThumb error: pnmscale binary %s not found.', $this->pnmscale_binary), LOG_ERR, __FILE__, __LINE__); $this->_valid_binaries = false; } if (!file_exists($this->cjpeg_binary)) { App::logMsg(sprintf('ImageThumb error: cjpeg binary %s not found.', $this->cjpeg_binary), LOG_ERR, __FILE__, __LINE__); $this->_valid_binaries = false; } } /** * Set the directory of the source images. * * @access public * @param string $source_dir The full directory path of the source images. * @return bool true on success, false on failure. */ function setSourceDirectory($source_dir) { // Set the source directory path, stripping any extra slashes if needed. $this->source_dir = preg_replace('!/+$!', '', $source_dir); if (!is_dir($this->source_dir)) { App::logMsg(sprintf('ImageThumb error: source directory not found: %s', $this->source_dir), LOG_ERR, __FILE__, __LINE__); return false; } if (!is_readable($this->source_dir)) { App::logMsg(sprintf('ImageThumb error: source directory not readable: %s', $this->source_dir), LOG_ERR, __FILE__, __LINE__); return false; } return true; } /** * Set the specification of thumbnails. * * @access public * @param array $spec The specifications for each size of output image. * @return bool true on success, false on failure. */ function setSpec($spec = array()) { $dest_dir = preg_replace('!/+$!', '', $spec['dest_dir']); $width = $spec['width']; $height = $spec['height']; $scaling_type = $spec['scaling_type']; $quality = isset($spec['quality']) ? $spec['quality'] : 75; $progressive = isset($spec['progressive']) ? $spec['progressive'] : false; $allow_upscaling = isset($spec['allow_upscaling']) ? $spec['allow_upscaling'] : false; $keep_filesize = isset($spec['keep_filesize']) ? $spec['keep_filesize'] : null; // Define pnmscale arguments. switch ($scaling_type) { case IMAGETHUMB_FIT_WIDTH : if (empty($width)) { App::logMsg('ImageThumb error: width not specified for IMAGETHUMB_FIT_WIDTH.', LOG_ERR, __FILE__, __LINE__); return false; } $pnmscale_args = sprintf(' -width %s ', escapeshellcmd($width)); break; case IMAGETHUMB_FIT_HEIGHT : if (empty($height)) { App::logMsg('ImageThumb error: height not specified for IMAGETHUMB_FIT_HEIGHT.', LOG_ERR, __FILE__, __LINE__); return false; } $pnmscale_args = sprintf(' -height %s ', escapeshellcmd($height)); break; case IMAGETHUMB_FIT_LARGER : if (empty($width) || empty($height)) { App::logMsg('ImageThumb error: width or height not specified for IMAGETHUMB_FIT_LARGER.', LOG_ERR, __FILE__, __LINE__); return false; } $pnmscale_args = sprintf(' -xysize %s %s ', escapeshellcmd($width), escapeshellcmd($height)); break; case IMAGETHUMB_STRETCH : if (empty($width) || empty($height)) { App::logMsg('ImageThumb error: width or height not specified for IMAGETHUMB_STRETCH.', LOG_ERR, __FILE__, __LINE__); return false; } $pnmscale_args = sprintf(' -width %s -height %s ', escapeshellcmd($width), escapeshellcmd($height)); break; case IMAGETHUMB_NO_SCALE : default : $pnmscale_args = ' 1 '; break; } // Define cjpeg arguments. $cjpeg_args = sprintf(' -optimize -quality %s ', escapeshellcmd($quality)); $cjpeg_args .= (true === $progressive) ? ' -progressive ' : ''; $this->spec[] = array( 'dest_dir' => $dest_dir, 'width' => $width, 'height' => $height, 'scaling_type' => $scaling_type, 'quality' => $quality, 'progressive' => $progressive, 'pnmscale_args' => $pnmscale_args, 'cjpeg_args' => $cjpeg_args, 'allow_upscaling' => $allow_upscaling, 'keep_filesize' => $keep_filesize, ); } /** * Make directory for each specified thumbnail size, if it doesn't exist. * * @access public * @return bool true on success, false on failure. */ function createDestDirs() { // Ensure we have a source. if (!isset($this->source_dir)) { App::logMsg(sprintf('Source directory not set before creating destination directories.'), LOG_ERR, __FILE__, __LINE__); return false; } $return_val = 0; foreach ($this->spec as $s) { if (!is_dir($this->source_dir . '/' . $s['dest_dir'])) { if (!mkdir($this->source_dir . '/' . $s['dest_dir'], $this->dest_dir_perms)) { $return_val += 1; App::logMsg(sprintf('mkdir failure: %s', $this->source_dir . '/' . $s['dest_dir']), LOG_ERR, __FILE__, __LINE__); } } } // If > 0, there was a problem creating dest dirs. return (0 == $return_val); } /** * Generate thumbnails for the specified file. * * @access public * @param string $file_name Name of file with extention. * @return bool true on success, false on failure. */ function processFile($file_name) { // Ensure we have valid binaries. if (!$this->_valid_binaries) { return false; } // Ensure we have a source. if (!isset($this->source_dir)) { App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__); return false; } // To keep this script running even if user tries to stop browser. ignore_user_abort(true); if (!ini_get('safe_mode')) { set_time_limit(300); } // Confirm source image exists. if (!file_exists($this->source_dir . '/' . $file_name)) { $this->raiseMsg(sprintf(_("Image resizing failed: source image not found: %s"), $file_name), MSG_ERR, __FILE__, __LINE__); App::logMsg(sprintf('Source image not found: %s', $file_name), LOG_ALERT, __FILE__, __LINE__); return false; } // Confirm source image is readable. if (!is_readable($this->source_dir . '/' . $file_name)) { $this->raiseMsg(sprintf(_("Image resizing failed: source image not readable: %s"), $file_name), MSG_ERR, __FILE__, __LINE__); App::logMsg(sprintf('Source image not readable: %s', $file_name), LOG_ALERT, __FILE__, __LINE__); return false; } // Confirm source image contains data. if (filesize($this->source_dir . '/' . $file_name) < 1) { $this->raiseMsg(sprintf(_("Image resizing failed: source image corrupt: %s"), $file_name), MSG_ERR, __FILE__, __LINE__); App::logMsg(sprintf('Source image is zero bytes: %s', $file_name), LOG_ALERT, __FILE__, __LINE__); return false; } // Confirm source image has a valid file extension. if (!$this->validFileExtension($file_name)) { $this->raiseMsg(sprintf(_("Image resizing failed: source image not of valid type: %s"), $file_name), MSG_ERR, __FILE__, __LINE__); App::logMsg(sprintf('Image resizing failed: source image not of valid type: %s', $file_name), LOG_ERR, __FILE__, __LINE__); return false; } // Output file will be a jpg. Set file extension. $file_name = substr($file_name, 0, strrpos($file_name, '.')) . '.jpg'; // This remains zero until something goes wrong. $final_return_val = 0; foreach ($this->spec as $s) { // Skip existing thumbnails with file size below $s['keep_filesize']. if (file_exists(realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name)) && isset($s['keep_filesize'])) { $file_size = filesize(realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name)); if ($file_size && $file_size < $s['keep_filesize']) { 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__); continue; } } // Determine if original file size is smaller than specified thumbnail size. Do not scale-up if allow_upscaling config is set to false. $image_size = getimagesize(realpath($this->source_dir . '/' . $file_name)); if ($image_size['0'] <= $s['width'] && $image_size['1'] <= $s['height'] && !$s['allow_upscaling']) { $pnmscale_args = ' 1 '; App::logMsg(sprintf('Image %s smaller than specified %s thumbnail size. Keeping original size.', $file_name, $s['dest_dir']), LOG_DEBUG, __FILE__, __LINE__); } else { $pnmscale_args = $s['pnmscale_args']; } // Execute the command that creates the thumbnail. $command = sprintf('%s %s/%s | %s %s | %s %s > %s/%s', escapeshellcmd($this->anytopnm_binary), escapeshellcmd($this->source_dir), escapeshellcmd($file_name), escapeshellcmd($this->pnmscale_binary), escapeshellcmd($pnmscale_args), escapeshellcmd($this->cjpeg_binary), escapeshellcmd($s['cjpeg_args']), escapeshellcmd(realpath($this->source_dir . '/' . $s['dest_dir'])), escapeshellcmd($file_name) ); App::logMsg(sprintf('ImageThumb command: %s', $command), LOG_DEBUG, __FILE__, __LINE__); exec($command, $output, $return_val); if (0 == $return_val) { // Make the thumbnail writable so the user can delete it over ftp without being 'apache'. chmod(realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name), $this->dest_file_perms); App::logMsg(sprintf('Successfully resized image %s', $s['dest_dir'] . '/' . $file_name, $return_val), LOG_DEBUG, __FILE__, __LINE__); } else { 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__); } // Return from the command will be > 0 if there was an error. $final_return_val += $return_val; } // If > 0, there was a problem thumbnailing. return (0 == $final_return_val); } /** * Process an entire directory of images. * * @access public * @return bool true on success, false on failure. */ function processAll() { // Ensure we have a source. if (!isset($this->source_dir)) { App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__); return false; } // Get all files in source directory. $dir_handle = opendir($this->source_dir); while ($dir_handle && ($file = readdir($dir_handle)) !== false) { // If the file name does not start with a dot (. or .. or .htaccess). if (!preg_match('/^\./', $file) && in_array(strtolower(substr($file, strrpos($file, '.') + 1)), $this->valid_file_extensions)) { $files[] = $file; } } // Process each found file. if (is_array($files) && !empty($files)) { foreach ($files as $file_name) { $this->processFile($file_name); } return sizeof($files); } else { App::logMsg(sprintf('No images found to thumbnail in directory %s.', $this->source_dir), LOG_NOTICE, __FILE__, __LINE__); return 0; } } /** * Delete the thumbnails for the specified file name. * * @access public * @param string $file_name The file name to delete, with extention. * @return bool true on success, false on failure. */ function deleteThumbs($file_name) { // Ensure we have a source. if (!isset($this->source_dir)) { App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__); return false; } $ret = 0; foreach ($this->spec as $s) { $file_path_name = realpath($this->source_dir . '/' . $s['dest_dir'] . '/' . $file_name); if (file_exists($file_path_name)) { if (!unlink($file_path_name)) { $ret++; App::logMsg(sprintf(_("Delete thumbs failed: %s"), $file_path_name), LOG_WARNING, __FILE__, __LINE__); } } } $this->raiseMsg(sprintf(_("The thumbnails for file %s have been deleted."), $file_name), MSG_SUCCESS, __FILE__, __LINE__); return (0 == $ret); } /** * Delete the source image with the specified file name. * * @access public * @param string $file_name The file name to delete, with extention. * @return bool true on success, false on failure. */ function deleteOriginal($file_name) { // Ensure we have a source. if (!isset($this->source_dir)) { App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__); return false; } $file_path_name = $this->source_dir . '/' . $file_name; if (!unlink($file_path_name)) { App::logMsg(sprintf(_("Delete original failed: %s"), $file_path_name), LOG_WARNING, __FILE__, __LINE__); return false; } $this->raiseMsg(sprintf(_("The original file %s has been deleted."), $file_name), MSG_SUCCESS, __FILE__, __LINE__); return true; } /** * Returns true if file exists. * * @access public * @param string $file_name The file name to test, with extention. * @return bool true on success, false on failure. */ function exists($file_name) { // Ensure we have a source. if (!isset($this->source_dir)) { App::logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__); return false; } return file_exists($this->source_dir . '/' . $file_name); } /** * Tests if extention of $file_name is in the array valid_file_extensions. * * @access public * @param string $file_name A file name. * @return bool True on success, false on failure. */ function validFileExtension($file_name) { preg_match('/.*?\.(\w+)$/i', $file_name, $ext); return in_array(strtolower($ext[1]), $this->valid_file_extensions); } /** * An alias for App::raiseMsg that only sends messages if display_messages is true. * * @access public * * @param string $message The text description of the message. * @param int $type The type of message: MSG_NOTICE, * MSG_SUCCESS, MSG_WARNING, or MSG_ERR. * @param string $file __FILE__. * @param string $line __LINE__. */ function raiseMsg($message, $type, $file, $line) { if ($this->display_messages) { App::raiseMsg($message, $type, $file, $line); } } } // End of class. ?>