* @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.
?>