The Zend Framework includes a class called Zend_Form_Element_File for creating a file upload within a form. It uses Zend_File_Transfer for receiving the file, but the whole process lacks some methods for the often required image upload.
What is easily possible
Let’s begin with the things that are possible. You can specify several validators for your file to check for file extensions or maximum file size. That’s stuff which is required for all files and therefore it is included. You may also specify a target directory.
// part of my form class (Default_Form_Photo::init) $photo = new Zend_Form_Element_File('photo'); $photo->setLabel('Photo') ->setDestination(Zend_Registry::get('config')->paths->backend->images->profile); // ensure only one file $photo->addValidator('Count', false, 1); // max 2MB $photo->addValidator('Size', false, 2097152) ->setMaxFileSize(2097152); // only JPEG, PNG, or GIF $photo->addValidator('Extension', false, 'jpg,png,gif'); $photo->setValueDisabled(true); $this->addElement($photo, 'photo'); |
Renaming a file according to your needs
Renaming a file according to your needs is also possible, even though (often) not as easily as the other stuff. You need to add a filter after initialising, because you usually do not know the filename at runtime. At least when you upload a profile picture you often want to give it a name like the username or the user’s id.
Therefore, you have to add the Rename-filter in your controller when a file is uploaded
// part of my controller after an upload (Default_UserController) if ($photo->getElement('photo')->isUploaded()) { $extension = pathinfo($photo->getElement('photo')->getValue(), PATHINFO_EXTENSION); $photo->getElement('photo')->addFilter('Rename', array( 'target' => $user->getUsername() . '.' . $extension, 'overwrite' => true )); if ($photo->getElement('photo')->receive()) { $profile = $user->getProfile(); $profile->setPicture($photo->getElement('photo')->getValue()); $profile->save(); } } |
This filter will rename the upload (only one file is allowed) according to the rule in target.
Resizing an image
The difficult part with Zend is resizing the image. Of course you can do this in your controller after you received the upload, but this is not very nice style. As Zend supports filters, we better program a new filter for this task. I called it Skoch_Filter_File_Resize:
<?php // Skoch/Filter/File/Resize.php /** * Zend Framework addition by skoch * * @category Skoch * @package Skoch_Filter * @license http://opensource.org/licenses/gpl-license.php GNU Public License * @author Stefan Koch <cct@stefan-koch.name> */ /** * @see Zend_Filter_Interface */ require_once 'Zend/Filter/Interface.php'; /** * Resizes a given file and saves the created file * * @category Skoch * @package Skoch_Filter */ class Skoch_Filter_File_Resize implements Zend_Filter_Interface { protected $_width = null; protected $_height = null; protected $_keepRatio = true; protected $_keepSmaller = true; protected $_directory = null; protected $_adapter = 'Skoch_Filter_File_Resize_Adapter_Gd'; /** * Create a new resize filter with the given options * * @param Zend_Config|array $options Some options. You may specify: width, * height, keepRatio, keepSmaller (do not resize image if it is smaller than * expected), directory (save thumbnail to another directory), * adapter (the name or an instance of the desired adapter) * @return Skoch_Filter_File_Resize An instance of this filter */ public function __construct($options = array()) { if ($options instanceof Zend_Config) { $options = $options->toArray(); } elseif (!is_array($options)) { require_once 'Zend/Filter/Exception.php'; throw new Zend_Filter_Exception('Invalid options argument provided to filter'); } if (!isset($options['width']) && !isset($options['height'])) { require_once 'Zend/Filter/Exception.php'; throw new Zend_Filter_Exception('At least one of width or height must be defined'); } if (isset($options['width'])) { $this->_width = $options['width']; } if (isset($options['height'])) { $this->_height = $options['height']; } if (isset($options['keepRatio'])) { $this->_keepRatio = $options['keepRatio']; } if (isset($options['keepSmaller'])) { $this->_keepSmaller = $options['keepSmaller']; } if (isset($options['directory'])) { $this->_directory = $options['directory']; } if (isset($options['adapter'])) { if ($options['adapter'] instanceof Skoch_Filter_File_Resize_Adapter_Abstract) { $this->_adapter = $options['adapter']; } else { $name = $options['adapter']; if (substr($name, 0, 33) != 'Skoch_Filter_File_Resize_Adapter_') { $name = 'Skoch_Filter_File_Resize_Adapter_' . ucfirst(strtolower($name)); } $this->_adapter = $name; } } $this->_prepareAdapter(); } /** * Instantiate the adapter if it is not already an instance * * @return void */ protected function _prepareAdapter() { if ($this->_adapter instanceof Skoch_Filter_File_Resize_Adapter_Abstract) { return; } else { $this->_adapter = new $this->_adapter(); } } /** * Defined by Zend_Filter_Interface * * Resizes the file $value according to the defined settings * * @param string $value Full path of file to change * @return string The filename which has been set, or false when there were errors */ public function filter($value) { if ($this->_directory) { $target = $this->_directory . '/' . basename($value); } else { $target = $value; } return $this->_adapter->resize($this->_width, $this->_height, $this->_keepRatio, $value, $target, $this->_keepSmaller); } } |
Adapter classes
As you might see this file also requires an adapter to ensure you can use the filter with both GD and Imagick. Thus, we need an abstract class and the implementation classes:
<?php // Skoch/Filter/File/Resize/Adapter/Abstract.php /** * Zend Framework addition by skoch * * @category Skoch * @package Skoch_Filter * @license http://opensource.org/licenses/gpl-license.php GNU Public License * @author Stefan Koch <cct@stefan-koch.name> */ /** * Resizes a given file and saves the created file * * @category Skoch * @package Skoch_Filter */ abstract class Skoch_Filter_File_Resize_Adapter_Abstract { abstract public function resize($width, $height, $keepRatio, $file, $target, $keepSmaller = true); protected function _calculateWidth($oldWidth, $oldHeight, $width, $height) { // now we need the resize factor // use the bigger one of both and apply them on both $factor = max(($oldWidth/$width), ($oldHeight/$height)); return array($oldWidth/$factor, $oldHeight/$factor); } } |
gd implementation
<?php // Skoch/Filter/File/Resize/Adapter/Gd.php /** * Zend Framework addition by skoch * * @category Skoch * @package Skoch_Filter * @license http://opensource.org/licenses/gpl-license.php GNU Public License * @author Stefan Koch <cct@stefan-koch.name> */ require_once 'Skoch/Filter/File/Resize/Adapter/Abstract.php'; /** * Resizes a given file with the gd adapter and saves the created file * * @category Skoch * @package Skoch_Filter */ class Skoch_Filter_File_Resize_Adapter_Gd extends Skoch_Filter_File_Resize_Adapter_Abstract { public function resize($width, $height, $keepRatio, $file, $target, $keepSmaller = true) { list($oldWidth, $oldHeight, $type) = getimagesize($file); switch ($type) { case IMAGETYPE_PNG: $source = imagecreatefrompng($file); break; case IMAGETYPE_JPEG: $source = imagecreatefromjpeg($file); break; case IMAGETYPE_GIF: $source = imagecreatefromgif($file); break; } if (!$keepSmaller || $oldWidth > $width || $oldHeight > $height) { if ($keepRatio) { list($width, $height) = $this->_calculateWidth($oldWidth, $oldHeight, $width, $height); } } else { $width = $oldWidth; $height = $oldHeight; } $thumb = imagecreatetruecolor($width, $height); imagealphablending($thumb, false); imagesavealpha($thumb, true); imagecopyresampled($thumb, $source, 0, 0, 0, 0, $width, $height, $oldWidth, $oldHeight); switch ($type) { case IMAGETYPE_PNG: imagepng($thumb, $target); break; case IMAGETYPE_JPEG: imagejpeg($thumb, $target); break; case IMAGETYPE_GIF: imagegif($thumb, $target); break; } return $target; } } |
Using the filter
This filter can now be attached to your Zend_Form_Element_File instance and will then resize the image to produce a thumbnail:
$photo->addFilter(new Skoch_Filter_File_Resize(array( 'width' => 200, 'height' => 300, 'keepRatio' => true, ))); |
You may specify several options invoking the filter. As you see in my code, I used with, height and keepRatio resulting in two maximum sizes. The image will then be resized so that it fits both of the lengths, but the aspect ratio will be kept. The whole list of options:
- width: The maximum width of the resized image
- height: The maximum height of the resized image
- keepRatio: Keep the aspect ratio and do not resize to both width and height (usually expected)
- keepSmaller: Do not resize if the image is already smaller than the given sizes
- directory: Set a directory to store the thumbnail in. If nothing is given, the normal image will be overwritten. This will usually be used when you produce thumbnails in different sizes.
- adapter: The adapter to use for resizing. You may specify a string or an instance of an adapter.
Now it’s easily possible to resize an uploaded image. To automatically load the classes, you need to add an option to your application.ini.
autoloaderNamespaces[] = "Skoch_"
Multiple thumbnails
Often you want to create several thumbnails in different sizes. This can be done by using a so called filter chain and the directory option of the Skoch_Filter_File_Resize.
If you specify directory, the value of setDestination() will not be considered anymore. Thus, you have to pass the full path to the directory option.
$filterChain = new Zend_Filter(); // Create one big image with at most 600x300 pixel $filterChain->appendFilter(new Skoch_Filter_File_Resize(array( 'width' => 600, 'height' => 300, 'keepRatio' => true, ))); // Create a medium image with at most 500x200 pixels $filterChain->appendFilter(new Skoch_Filter_File_Resize(array( 'directory' => '/var/www/skoch/upload/medium', 'width' => 500, 'height' => 200, 'keepRatio' => true, ))); // Rename the file, of course this should not be a fixed string in real applications $multiResize->addFilter('Rename', 'users_upload'); // Add the filter chain with both resize rules $multiResize->addFilter($filterChain); |
Download
You can download the library and a tiny example from my github repository.
Caveats
If you want to use the directory option together with renaming, make sure to add the Resize-filter after the Rename-filter to ensure that Resize gets the new filename and will save the thumbnail with the new filename. Otherwise you might get this structure:
/img/gallery/stefan/thumbs/Spain_1000.png /img/gallery/stefan/1234.png
Where you probably do not want to have the filename Spain_1000.png on your server
So don’t forget to add Resize after Rename.
First of all thanks for this filter, I was on the way to write mine when i found yours
.
i’ve added few lines to be able to manage transparency for PNG & GIF:
Again thx for you’re work, hope this addon will help you or others.
Dede
Awesome
If you have any other additions, feel free to add them.
I also found a new caveat, you need to add the rename filter before my resize-filter if you use the directory-option, otherwise the file will be stored according to the name on the user’s computer.
I added your lines to my code above.
Great Work, thank you very much for sharing this with us. I just got an urgent question, I am trying to re-size the image 3 times. I learnt that when you add a directory value, you get the image in 2 sizes, but one size is the original, which would than be out of my control.
Is there a way to re-size multiple times with your filter? I can’t figure it out, but have the feeling it is not possible. Still great work ! Luka
Hi Luka, you are asking at the right time as I had the same problem in a current project. You can create multiple sizes at the same time with a filter chain:
If you have any problems, just ask again. You have to use a filter chain, because Zend_File_Transfer does not allow you to add a filter from the same class twice (because it stores them in an array with class-name as index).
Cool, that works great. Thank you so much !! Might be a rather silly question, but would you know if it’s also possible to give them different names? My script was already set up to work with different names for the different sizes and it would be great, if I don’t have to rewite all the code… Thank you again.
Luka
Hi Luka, this is just an idea, I have not tried it. Maybe you can create sort of groups within the filter chain by creating 3 filter chains which contain each one Zend_Filter_File_Rename and one Skoch_Filter_File_Resize. Then the rename filter could rename it before the resize filter would store it. When the parser leaves the chain, everything would fall back and in the next sub-chain another rename-filter would rename it.
In Pseudocode it could look like this:
chain1 = new Zend_Flter();chain1->add(new Zend_Filter_File_Rename(name1));
chain1->add(new Skoch_Filter_File_Resize());
chain2 = new Zend_Flter();
chain2->add(new Zend_Filter_File_Rename(name2));
chain2->add(new Skoch_Filter_File_Resize());
chain3 = new Zend_Flter();
chain3->add(new Zend_Filter_File_Rename(name3));
chain3->add(new Skoch_Filter_File_Resize());
chain = new Zend_Filter();
chain->add(chain1);
chain->add(chain2);
chain->add(chain3);
photoUpload->addFilter(chain);
If that does not work, you can always rewrite the filter class to allow a new parameter called ‘filename’. I just did not add it, because there is the Rename filter.
You would have to add in the construct of Skoch_Filter_File_Resize:
And then in filter:
Hope this helps.
Actually, the line:
$target = dirname($target) . ‘/’ . $this->_filename;
should be
$target = dirname($value) . ‘/’ . $this->_filename;
By the way, very nice work, man. Thanks a lot!!!
Great, I got it to work. I have choosen your second solution, because I could not get the first one to work.
I only had to do a small change
from: $target = dirname($target) . ‘/’ . $this->_filename;
to: $target = $this->_filename;
Now it all works. Thank you so much, I was looking for ages to find a resize Function. Great Work !
Hello stefan
Thank you for this codes. I use your codes and I have little problem whit this. when I set resize images in 3 or 4 size and I use your filterChain codes, if image width or height is smaller than my set size resize don’t work and retun arrey if width and height is bigger it work whit out problem.
And do you have any idea about get Image Dimensions and size whit this filter?
I just tried to reproduce it, but here it works fine. I used an image of 100×56 and the target values were 600×300 and 500×200. I tried it with both
keepSmaller = trueandkeepSmaller = falseand it worked in both cases as expected.Have you found some solution for your problem or can you provide further information?
Hello!
How can a filter be used twice in one image?
The code below executes only the second filter…
$file = new Zend_Form_Element_File(‘file’);
$file->setLabel(‘Upload File’)
->addFilter(new Square_Filter_File_Resize(array(
‘width’ => 200,
‘height’ => 300,
‘keepRatio’ => true,
‘directory’ => APPLICATION_PATH . ‘/../public/uploads’,
)))
->addFilter(new Square_Filter_File_Resize(array(
‘width’ => 20,
‘height’ => 30,
‘keepRatio’ => true,
‘directory’ => APPLICATION_PATH . ‘/../public/uploads/thumbs’,
)))
->setRequired(true);
Thanx!
Hi Bumbar, if I understand you correctly, this is possible with Filter-Chains.
Have a look at this answer on Luka’s question: http://eliteinformatiker.de/2011/09/02/thumbnails-upload-and-resize-images-with-zend_form_element_file/#comment-416
This is at least how I currently create 3 sizes of images.
This work only one image.
How do that work in more of the one image?
$adapter = new Zend_File_Transfer_Adapter_Http();
$adapter->addFilter(new Zend_Filter_File_Resize(array(
‘width’ => 440,
‘height’ => 300,
‘keepRatio’ => false,
)));
$adapter->addFilter(new Zend_Filter_File_Resize(array(
‘directory’ => $thumb,
‘width’ =>100,
‘height’ => 100,
‘keepRatio’ => false,
)));
Don’t work replace the second image with thumb
I want know if have a solution to work with multiple images on upload adapter.
Thanks
Hi Marcio,
I use a filter chain.
Hi,
I don’t understand why that doesn’t work with my filters chain, look here :
http://stackoverflow.com/questions/14054108/how-skochs-resizing-system-works
Thank you
Resolved !
Same link, http://stackoverflow.com/questions/14054108/how-skochs-resizing-system-works
Glad you found the solution. I did not see your question, because I do not have my blog e-mail in my netbook’s Thunderbird.
From reading your solution, it seems I should really refactor my Skoch_Resize soon. Seems quite buggy and that task has been on my wishlist for so long :/ And now the system becomes more and more popular, so it should not be buggy…
Hello to all.
I have a problem. I am not able to operate the filter.
I added in library the directory Skoch, I added the option in application.ini.
I send the photo, is renamed and saved in the directory, but not resized.
I guess I do not call the method receive properly or have to do an extra step. But where?
I have:
class Site_Form_MyElement_Photo extends Zend_Form_Element_File
{
public function __construct($nome)
{
parent::__construct($nome);
$this->setLabel(‘Load photo:’)
$this->addFilter(new Skoch_Filter_File_Resize(array(
‘width’ => 200,
‘height’ => 300,
‘keepRatio’ => true,
)));
}
}
Then
class Site_Form_Photo extends Zend_Form
{
function __construct()
{
$argv = func_get_args();
switch( func_num_args() )
{
case 0:
self::__construct1(NULL);
break;
case 1:
self::__construct1($argv[0]);
break;
}
}
public function __construct1($id_a)
{
parent::__construct();
$this->setAction(‘/album/photo/new’)
->setMethod(‘post’);
$name = new Site_Form_MyElement_NameFile(‘name_photo’);
$name->setLabel(‘Name Photo:’);
$image = new Site_Form_MyElement_Photo(‘image’);
$id_album = new Site_Form_MyElement_HiddenId(‘id_album’);
if(is_numeric($id_a))
$id_album->setValue($id_a);
$submit = new Zend_Form_Element_Submit(‘submit’);
$submit->setLabel(‘Ok’);
// Aggiunge gli elementi al form per comporlo
$this->addElement($name)
->addElement($image)
->addElement($id_album)
->addElement($submit);
}
}
The controller
class PhotoController extends Zend_Controller_Action
{
public function preDispatch()
{
$filters = array(
‘numberAlbum’ => array(‘HtmlEntities’, ‘StripTags’, ‘StringTrim’)
);
$validators = array(
‘numberAlbum’ => array(‘NotEmpty’, ‘Int’)
);
$input = new Zend_Filter_Input($filters, $validators);
$input->setData($this->getRequest()->getParams());
$model_album = new Model_Album();
if(isset($input->numeroAlbum))
{
$this->album = $model_album->getAlbum($input->numberAlbum);
$this->id_album = $input->numberAlbum;
}
}
public function newAction()
{
$form = new Sito_Form_Foto($this->id_album);
$this->view->form = $form;
if($this->getRequest()->isPost())
{
if($form->isValid($this->getRequest()->getPost()))
{
$model_album = new Model_Album();
$album = $model_album->getAlbum($form->getValue(‘id_album’));
if ($album instanceof Site_Class_Album)
{
$config = $this->getInvokeArg(‘bootstrap’)->getOption(‘uploads’);
$form->immagine->setDestination($config['uploadPath']);
$adapter = $form->immagine->getTransferAdapter();
if($adapter->getMimeType()!=NULL)
{
// Rename file
$name_file = uniqid(md5(time()));
$xt = @pathinfo($adapter->getFileName(‘image’), PATHINFO_EXTENSION);
$adapter->clearFilters();
$adapter->addFilter(‘Rename’, array(
‘target’ => sprintf(‘%s.%s’,$nome_file, $xt),
‘overwrite’ => true));
if($adapter->receive(‘immagine’))
{
$name_file = $name_file.’.’.$xt;
$date_insert = date(‘Y-m-d’, time());
$photo = new Site_Class_Photo($form->getValue(‘name_photo’), $name_file,
$date_insert, $album);
$model_photo = new Model_Photo();
if($model_foto->savePhoto($photo)==0)
{
$this->_helper->getHelper(‘FlashMessenger’)->addMessage(‘Foto ‘.$foto->getName().’ added.’);
$this->_redirect(‘/album/photo/success’);
}
else
{
$this->_helper->getHelper(‘FlashMessenger’)->addMessage(“Error.”);
$this->_redirect(‘/album/photo/error’);
}
}
else
{
$this->_helper->getHelper(‘FlashMessenger’)->addMessage(“Error.”);
$this->_redirect(‘/album/photo/error’);
}
}
else
{
$this->_helper->getHelper(‘FlashMessenger’)->addMessage(“Error.”);
$this->_redirect(‘/album/photo/error’);
}
}
else
{
$this->_helper->getHelper(‘FlashMessenger’)->addMessage(“Error.”);
$this->_redirect(‘/album/photo/error’);
}
}
}
}
}
Thank you in advance.
Pingback: Zend Framework: Uploading and resizing an image | Saunders Web Solutions Blog