ActAs Image column behavior

By Yevgeny Tomenko (SkieDr)
This behavior add new column to your model and allow to store images in file system. It can resize images, create several versions, and thubnails. After find model you got link to file for each record in model.
Samples of using this behavior you see there:
image_bahavior_example.rar

Download code
<?php

/* 
 * Image for cakePHP 
 * comments, bug reports are welcome skie AT mail DOT ru 
 * @author Yevgeny Tomenko aka SkieDr 
 * @version 1.0.0.5 

files stored in structure 
/images/{models}/{$id}/{field}.ext

 */ 

class ImageBehavior extends ModelBehavior {

    var 
$settings null;

    function 
setup(&$model$config = array()) {
        
$this->imageSetup(&$model$config);
    }
    
    function 
imageSetup(&$model$config = array()) {
        
$settings Set::merge(array(
        
'baseDir'=> '',
        ), 
$config);
        
        if (!isset(
$settings['fields'])) $settings['fields']=array();
        
$fields=array();
        foreach(
$settings['fields'] as $key=>$value) {
            
$field ife(is_numeric($key), $value$key);
            
$conf ife(is_numeric($key), array(), ife(is_array($value),$value,array()));
            
$conf=Set::merge(
            array (
                
'thumbnail' => array('prefix'=>'thumb',
                             
'create'=>false,
                             
'width'=>'100',
                              
'height'=>'100',
                              
'aspect'=>true,
                             
'allow_enlarge'=>true,
                            ),
                
'resize'=>null// array('width'=>'100','heigth'=>'100'),        
                
'versions' => array(
                ),
            ), 
$conf);
            foreach (
$conf['versions'] as $id=>$version) {
                
$conf['versions'][$id]=Set::merge(array(
                              
'aspect'=>true,
                             
'allow_enlarge'=>false,
                            ),
$version);                
            }
            if (
is_array($conf['resize'])) {
                if (!isset(
$conf['resize']['aspect'])) $conf['resize']['aspect']=true;
                if (!isset(
$conf['resize']['allow_enlarge'])) $conf['resize']['allow_enlarge']=false;
            }
            
$fields[$field]=$conf;
            
        }
        
$settings['fields']=$fields;
        
        
$this->settings[$model->name] = $settings;
    }
    
    
/**
     * Before save method. Called before all saves
     *
     * Overriden to transparently manage setting the item position to the end of the list 
     *
     * @param AppModel $model
     * @return boolean True to continue, false to abort the save
     */ 
    
function beforeSave(&$model) {
        
extract($this->settings[$model->name]);
        if (empty(
$model->data[$model->name][$model->primaryKey])) {
        }

        
        
$tempData = array();
        foreach (
$fields as $key=>$value) {
            
$field ife(is_numeric($key), $value$key);
            if (isset(
$model->data[$model->name][$field])) {
                if (
$this->__isUploadFile($model->data[$model->name][$field])) {
                    
$tempData[$field] = $model->data[$model->name][$field];
                    
$model->data[$model->name][$field]=$this->__getContent($model->data[$model->name][$field]);
                } else {
                    unset(
$model->data[$model->name][$field]);
                }
            }
        }
        
        
$this->runtime[$model->name]['beforeSave'] = $tempData;         
        return 
true;
    } 

    function 
afterSave(&$model) {
        
extract($this->settings[$model->name]);
        if (empty(
$model->data[$model->name][$model->primaryKey])) {
        }

        
$tempData $this->runtime[$model->name]['beforeSave']; 
        unset(
$this->runtime[$model->name]['beforeSave']);
        foreach(
$tempData as $field=>$value) {
            
$this->__saveFile(&$model$field$value);
        }
        
        return 
true;
    } 

    
    function 
afterFind(&$model, &$results$primary) { 
        
extract($this->settings[$model->name]);

        if ( 
is_array$results ) ) {
            
$i=0;
            if (isset(
$results[0])) {
                        while ( isset( 
$results[$i][$model->name] ) && is_array$results[$i][$model->name] ) )  {
                foreach (
$fields as $field => $fieldParams) {
                    if (isset(
$results[$i][$model->name][$field]) && ($results[$i][$model->name][$field]!='')) {
                        
$value=$results[$i][$model->name][$field];
                        
$results[$i][$model->name][$field]=$this->__getParams(&$model$field$value,$fieldParams$results[$i][$model->name]);
                    }
                }
                
$i++;
                
                }                     
            } else {
                foreach (
$fields as $field => $fieldParams) {
                    if (isset(
$results[$model->name][$field]) && ($results[$i][$model->name][$field]!='')) {
                        
$value=$results[$i][$model->name][$field];
                        
$results[$model->name][$field]=$this->__getParams(&$model$field$value$fieldParams$results[$model->name]);
                    }
                }
            }
        }        
        return 
true;
    }     
    
    function 
__getParams(&$model$field$value$fieldParams$record) {
        
extract($this->settings[$model->name]);
        
$result=array();
        if (
$value!='') {
            
$folderName $this->__getFolder(&$model$record);
            
$ext=$this->decodeContent($value);
            
$fileName=$field .'.'$ext;
            
$result['path']=$folderName.$fileName;
            
            
$thumb=$fields[$field]['thumbnail'];
            if (
$thumb['create']) {
                
$result['thumb']=$folderName.$this->__getPrefix($thumb).'_'.$fileName;
            }
            foreach(
$fields[$field]['versions'] as $version) {
                
$result[$this->__getPrefix($version)]=$folderName.$this->__getPrefix($version).'_'.$fileName;
            }
        }
        return 
$result;
    }
    
    
/**
     * Before delete method. Called before all deletes
     *
     * Will delete the current item from list and update position of all items after one
     *
     * @param AppModel $model
     * @return boolean True to continue, false to abort the delete
     */ 
    
function beforeDelete(&$model) {
        
$this->runtime[$model->name]['ignoreUserAbort'] = ignore_user_abort();
        @
ignore_user_abort(true);
        return 
true;
    } 

    function 
afterDelete(&$model) { 
        
extract($this->settings[$model->name]);
        
        foreach (
$fields as $field=>$fieldParams) {
            
$folderPath=$this->__getFullFolder(&$model$field);
            
uses ('folder'); 
            
$folder = &new Folder($path $folderPath$create false);
            if (
$folder!==false) {
                @
$folder->delete($folder->pwd());
            }            
        }
        
        @
ignore_user_abort((bool) $this->runtime[$model->name]['ignoreUserAbort']);
        unset(
$this->runtime[$model->name]['ignoreUserAbort']); 
        return 
true;
    }     
    
    function 
__isUploadFile($file) {
        if (!isset(
$file['tmp_name'])) return false;
        return (
file_exists($file['tmp_name']) && $file['error']==0);
    }

    function 
__getContent($file) {
        return 
$file['type'];
    }
    function 
decodeContent($content) {
        
$contentsMaping=array(
          
"image/gif" => "gif",
          
"image/jpeg" => "jpg",
          
"image/pjpeg" => "jpg",
          
"image/x-png" => "png",
          
"image/jpg" => "jpg",
          
"image/png" => "png",
          
"application/x-shockwave-flash" => "swf",
          
"application/pdf" => "pdf",
          
"application/pgp-signature" => "sig",
          
"application/futuresplash" => "spl",
          
"application/msword" => "doc",
          
"application/postscript" => "ps",
          
"application/x-bittorrent" => "torrent",
          
"application/x-dvi" => "dvi",
          
"application/x-gzip" => "gz",
          
"application/x-ns-proxy-autoconfig" => "pac",
          
"application/x-shockwave-flash" => "swf",
          
"application/x-tgz" => "tar.gz",
          
"application/x-tar" => "tar",
          
"application/zip" => "zip",
          
"audio/mpeg" => "mp3",
          
"audio/x-mpegurl" => "m3u",
          
"audio/x-ms-wma" => "wma",
          
"audio/x-ms-wax" => "wax",
          
"audio/x-wav" => "wav",
          
"image/x-xbitmap" => "xbm",             
          
"image/x-xpixmap" => "xpm",             
          
"image/x-xwindowdump" => "xwd",             
          
"text/css" => "css",             
          
"text/html" => "html",                          
          
"text/javascript" => "js",
          
"text/plain" => "txt",
          
"text/xml" => "xml",
          
"video/mpeg" => "mpeg",
          
"video/quicktime" => "mov",
          
"video/x-msvideo" => "avi",
          
"video/x-ms-asf" => "asf",
          
"video/x-ms-wmv" => "wmv"
        
);
        if (isset(
$contentsMaping[$content]))
            return 
$contentsMaping[$content];
        else return 
$content;
    }
    
    
    function 
__saveAs($fileData$fileName=null$folder) {
        
        if (
is_writable($folder)) {
            if (
is_uploaded_file($_FILES[$fileData]['tmp_name'])) 
            {
                if (empty(
$fileName)) $fileName $_FILES[$fileData]['name'];
                
copy($_FILES[$fileData]['tmp_name'], $folder.$fileName);
                return 
true;
            }
            else
            {
                return 
false;
            }
        }
        else
        {
            return 
false;
        }
    }
    
    function 
__getFolder(&$model$record) {
        
extract($this->settings[$model->name]);
        return  
$baseDir .'/'Inflector::camelize($model->name) .'/'$record[$model->primaryKey] . '/';
    }
    function 
__getFullFolder(&$model$field) {
        
extract($this->settings[$model->name]);
        return  
WWW_ROOT IMAGES_URL$baseDir .DSInflector::camelize($model->name) .DS$model->id .DS;
    }
    
    function 
__saveFile(&$model$field$fileData) {
        
extract($this->settings[$model->name]);
        
$folderName $this->__getFullFolder(&$model$field);
        
$ext=$this->decodeContent($this->__getContent($fileData));
        
$fileName=$field .'.'$ext;

        
uses ('folder'); 
        
uses ('file'); 
        
$folder = &new Folder($path $folderName$create true$mode '777');
        
        
$files=$folder->find($fileName);
        
        
$file= &new File($folder->pwd().DS.$fileName);
        
        
$fileExists=($file!==false);
        if (
$fileExists) { 
            @
$file->delete();
        } 
        
        if (isset(
$fields[$field]['resize']['width']) && isset($fields[$field]['resize']['height'])) {    
            
$file=$folder->pwd().DS.'tmp_'.$fileName;
            
copy($fileData['tmp_name'], $file);
            
$this->__resize($folder->pwd(),'tmp_'.$fileName,$fileName,$field$fields[$field]['resize']);
            @
unlink($file);            
        } else {        
            
$file=$folder->pwd().DS.$fileName;
            
copy($fileData['tmp_name'], $file);
        }

        
        
        if (
$fields[$field]['thumbnail']['create']) {
            
$fieldParams=$fields[$field]['thumbnail'];
            
$newFile=$this->__getPrefix($fieldParams).'_'.basename($fileName);
            
$this->__resize($folder->pwd(),$fileName,$newFile$field$fieldParams);
        }
        foreach(
$fields[$field]['versions'] as $version) {
            
$fieldParams=$fields[$field]['thumbnail'];
            
$newFile=$this->__getPrefix($version).'_'.basename($fileName);
            
$this->__resize($folder->pwd(),$fileName,$newFile,$field$version);
        
        }
        
    }
    
    
    function 
__getPrefix($fieldParams) {
        if (isset(
$fieldParams['prefix'])) {
            return 
$fieldParams['prefix'];
        } else {
            return 
$fieldParams['width'].'x'.$fieldParams['height'];
        }
    }
    
    
/** 
     * Automatically resizes an image and returns formatted IMG tag 
     * 
     * @param string $path Path to the image file, relative to the webroot/img/ directory. 
     * @param integer $width Image of returned image 
     * @param integer $height Height of returned image 
     * @param boolean $aspect Maintain aspect ratio (default: true) 
     * @param array    $htmlAttributes Array of HTML attributes. 
     * @param boolean $return Wheter this method should return a value or output it. This overrides AUTO_OUTPUT. 
     * @return mixed    Either string or echos the value, depends on AUTO_OUTPUT and $return. 
     * @access public 
     */ 
    
function __resize($folder$originalName$newName$field$fieldParams) { 
         
        
$types = array(=> "gif""jpeg""png""swf""psd""wbmp"); // used to determine image type 
        
$fullpath $folder
     
        
$url $folder.DS.$originalName
         
        if (!(
$size getimagesize($url)))  
            return; 
// image doesn't exist 
             
        
$width=$fieldParams['width'];
        
$height=$fieldParams['height']; 
        if (
$fieldParams['allow_enlarge']===false) { // don't enlarge image
            
if (($width>$size[0])||($height>$size[1])) {
                
$width=$size[0];
                
$height=$size[1]; 
            }
        } else {
            if (
$fieldParams['aspect']) { // adjust to aspect. 
                
if (($size[1]/$height) > ($size[0]/$width))  
                    
$width ceil(($size[0]/$size[1]) * $height); 
                else  
                    
$height ceil($width / ($size[0]/$size[1])); 
            } 
        }
  
        
$cachefile $fullpath.DS.$newName;  // location on server 
         
        
if (file_exists($cachefile)) { 
            
$csize getimagesize($cachefile); 
            
$cached = ($csize[0] == $width && $csize[1] == $height); // image is cached 
            
if (@filemtime($cachefile) < @filemtime($url)) // check if up to date 
                
$cached false
        } else { 
            
$cached false
        } 
         
        if (!
$cached) { 
            
$resize = ($size[0] > $width || $size[1] > $height) || ($size[0] < $width || $size[1] < $height || ($fieldParams['allow_enlarge']===false)); 
        } else { 
            
$resize false
        } 
         
        if (
$resize) { 
            
$image call_user_func('imagecreatefrom'.$types[$size[2]], $url); 
            if (
function_exists("imagecreatetruecolor") && ($temp imagecreatetruecolor ($width$height))) { 
                
imagecopyresampled ($temp$image0000$width$height$size[0], $size[1]); 
              } else { 
                
$temp imagecreate ($width$height); 
                
imagecopyresized ($temp$image0000$width$height$size[0], $size[1]); 
            } 
            
call_user_func("image".$types[$size[2]], $temp$cachefile); 
            
imagedestroy ($image); 
            
imagedestroy ($temp); 
        }          
         
    } 

    
}    
?>

 

Comments 510

CakePHP Team Comments Author Comments
 

Comment

1 examples

Posted Aug 26, 2007 by Yevgeny Tomenko
 

Comment

2 Very nice

I was working on something similar, but using an Image model to store the file info and associate the image with parent model using a belongsTo association. Your solution is much nicer.

I was also able to modify the behavior to make it work for both image and file uploads.

Thanks!

Posted Aug 31, 2007 by Scott Brisko
 

Question

3 How to check extensions

I have just started my adventure with cake and could You tell how to validate avantar extension . For example when I'am trying to upload zip file I wish to receive flash message: "wrong type of file".
Posted Sep 9, 2007 by Patryk
 

Comment

4 Aspect ratio

Thanks for this piece of code, Yevgeny!

How do I go about passing just one of the dimensions and having the behavior create the image such that the aspect ratio is preserved?
I tried not passing 'height' for the versions, but I got errors and the version images were not created.

Thanks in advance!
Posted Sep 24, 2007 by Atanas Vasilev
 

Comment

5 Nice but

...unfortunatly this behaviour isnt working on recursive associations. Though this isnt caused by the behaviour itself but by the fact that CAKE doesnt seem to call behaviours recursivly it is still a big limitation. I wonder if there is a way to make behaviours work on recursive associated models without modifying the CAKE core files.
Posted Oct 24, 2007 by Nikolas Hagelstein
 

Question

6 License

Hi SkieDr!

There is no info about the license and I would like to modify this behaviour to use it in a personal project, may I?

BTW, good job mate, this code is very useful :-)
Posted Nov 1, 2007 by Paolo Stancato
 

Comment

7 Validation only image types

Use the beforeValidate() function below to check if the uploaded image is really an image.. invalidates if not...

Just paste it somewhere in the behavior above.

Haven't tested it that much but it works fine for me!

http://www.pastebin.be/6872

cheers!
Posted Nov 20, 2007 by Jasper
 

Comment

8 imagemagick

Would be great to move the GD stuff out of __resize() and put them into __resize_gd2() and then create __resize_imagemagick().

This would allow the use of the far superior imagemagick over gd, and would also be easily extend with other image lib functions if needed.

p.s. Awesome code, thanks!
Posted Dec 23, 2007 by Brett ODonnell
 

Comment

9 specify upload folder

I have some questions about defining the upload folder used in these 2 functions.

<code>
function __getFolder(&$model, $record) {
extract($this->settings[$model->name]);
return $baseDir .'/'. Inflector::camelize($model->name) .'/'. $record[$model->primaryKey] . '/';
}
function __getFullFolder(&$model, $field) {
extract($this->settings[$model->name]);
return WWW_ROOT . IMAGES_URL. $baseDir .DS. Inflector::camelize($model->name) .DS. $model->id .DS;
}
</code>


Where do $baseDir and IMAGES_URL get defined?
Posted Dec 23, 2007 by Brett ODonnell
 

Bug

10 Storing file type

For some reason, the file type is being stored in the DB field rather than the file name. I'm using CakePHP 1.2.0.6311-beta
Posted Jan 14, 2008 by Steve Oliveira
 

Comment

11 Preserving aspect ratio

How do I go about passing just one of the dimensions and having the behavior create the image such that the aspect ratio is preserved?
I tried not passing 'height' for the versions, but I got errors and the version images were not created.


It is quite easy to modify the behavior to make it generate the other dimension when only one is passed. In __resize() method just before the $width and $height declaration add this code:


        if (!isset($fieldParams['height']) or !$fieldParams['height']) {
            $fieldParams['height'] = (int) (($fieldParams['width'] / $size[0]) * $size[1]);
        } elseif (!isset($fieldParams['width']) or !$fieldParams['width']) {
            $fieldParams['width'] = (int) (($fieldParams['height'] / $size[1]) * $size[0]);
        }
Posted Jan 16, 2008 by Yura
 

Question

12 How can I get the images when the model comes in as a recursive association

Is that possible at all ?
Posted Jan 18, 2008 by Sven
 

Bug

13 Incorrect afterFind declaration

There is problem with afterFind method that leads to impossibility of accessing this behavior through association (using https://trac.cakephp.org/ticket/2056 or http://groups.google.com/group/cake-php/browse_thread/thread/5f3d90e3686f191b/c3bb61dfb902b8c9?lnk=gst&q=afterfind#c3bb61dfb902b8c9 patch that makes it possible).
This method should return $results instead of boolean.
Change method declaration from:
function afterFind(&$model, &$results, $primary)
to:
function afterFind(&$model, $results, $primary)
And the last statement in it from:
return true;
to:
return $results;
Posted Feb 25, 2008 by Artem Gluvchynskyj
 

Bug

14 Reduntant cycle counter in afterFind()

Perhaps in afterFind(), this code:


} else {
    foreach ($fields as $field => $fieldParams) {
        if (isset($results[$model->name][$field]) && ($results[$i][$model->name][$field]!='')) {
            $value=$results[$i][$model->name][$field];
            $results[$model->name][$field]=$this->__getParams(&$model, $field, $value, $fieldParams, $results[$model->name]);
        }
    }
}


should be changed to this:


} else {
    foreach ($fields as $field => $fieldParams) {
        if (isset($results[$model->name][$field]) && ($results[$model->name][$field]!='')) {
            $value=$results[$model->name][$field];
            $results[$model->name][$field]=$this->__getParams(&$model, $field, $value, $fieldParams, $results[$model->name]);
        }
    }
}
Posted Mar 8, 2008 by kAtremer
 

Comment

15 deleting an image

I need to delete a photo and I have modified beforeSave(), in the view I put a delete field ("delete_"+field name) like this:

echo $form->input('delete_fot1',array('type' => 'checkbox'));


and the beforeSave() could be like this:

    function beforeSave(&$model) {
        //$model->log($this->settings);
        extract($this->settings[$model->name]);
        if (empty($model->data[$model->name][$model->primaryKey])) {
            //$this->__addToListBottom(&$model);
        }
        
        $tempData = array();
        foreach ($fields as $key=>$value) {
            $field = ife(is_numeric($key), $value, $key);
            if (isset($model->data[$model->name][$field])) {
                if ($this->__isUploadFile($model->data[$model->name][$field])) {
                    $tempData[$field] = $model->data[$model->name][$field];
                    $model->data[$model->name][$field]=$this->__getContent($model->data[$model->name][$field]);
                } else {
                    //delete image 
                    if (isset($model->data[$model->name]["delete_".$field])) {
                        if($model->data[$model->name]["delete_".$field]==1){
                            
                            @unlink($this->__getFullFolder(&$model, $field).'/'.$field.'.png');
                            @unlink($this->__getFullFolder(&$model, $field).'/thumb_'.$field.'.png');
                            $model->data[$model->name][$field]="";
                            
                        }else{
                            unset($model->data[$model->name][$field]);
                        }
                    }else{
                        unset($model->data[$model->name][$field]);
                    }
                    
                }
            }
        }
         //debug($model->data);
         //debug($tempData);        
        $this->runtime[$model->name]['beforeSave'] = $tempData;         
        return true;
    } 


Posted Jun 3, 2008 by juanma
 

Comment

16 Just a little orthographic error

You wrote "thubnails" instead of thumbnails.
I can't find any other error: your behavior is great
Posted Jun 16, 2008 by Arialdo Martini
 

Comment

17 Version Compatibility

great code but this will not work out of the box in

1.2.0.7125

perhaps add a little note at the top to see what version this was written for.

Posted Jun 23, 2008 by Arnold Almeida