I know, I know... I promised it long time ago but I decided to take some time off from coding, not even mentioning nerve-wracking Magento(; But I'm back in business now so here is your short guide on how to crop your images. You can use it for almost everything but it's great for designs with square (ish) products images.

Module File StructureBefore we start - there's one thing worth mentioning. I haven't figured out a way to extend Varien classes so if you know any - leave a comment below - otherwise you'll have to keep an eye on Image.php and Gd2.php while upgrading. But anyway... first thing you want to do is to create a file structure matching the image on your right hand side. Remember that everything in Magento tends to complain and die unexpectedly because of case sensitivity, so if you want to use your own names keep the uppercase in the same places I did. Whenever you're ready, launch your IDE and proceed with making your hands dirty(; or, alternatively, grab the whole package from here and dig through the code on your own.

If you researched this matter before you probably know that the crop method is actually present in the Gd2 class - perhaps not as pretty and sexy as it could be, but still. In fact the only thing missing is the whole linkage between blocks and image processor. For purposes of this guide I decided to create an alternative frontCrop method and leave original intact, just in case it was used by some other classes. So... the first thing you want to do is to extend the Product_Image Model class - I'm not going to get into details of what each line does - enough to say it takes care of queuing of all transformations applied to the image, checking if the image exists and passing it to the image processor. Make sure you included all non-public methods to avoid obvious errors - it may change after each upgrade!
class Criography_ExtendedProductImage_Model_Product_Image extends Mage_Catalog_Model_Product_Image
{
    protected $_cropDims;

 
    /**
     * Set filenames for base file and new file
     *
     * @param string $file
     * @return Mage_Catalog_Model_Product_Image
     */
    public function setBaseFile($file)
    {
        $this->_isBaseFilePlaceholder = false;

        if (($file) && (0 !== strpos($file, '/', 0))) {
            $file = '/' . $file;
        }
        $baseDir = Mage::getSingleton('catalog/product_media_config')->getBaseMediaPath();

        if ('/no_selection' == $file) {
            $file = null;
        }
        
        
        if ($file) {
            if ((!file_exists($baseDir . $file)) || !$this->_checkMemory($baseDir . $file)) {
                $file = null;
            }
        }
        if (!$file) {
            // check if placeholder defined in config
            $isConfigPlaceholder = Mage::getStoreConfig("catalog/placeholder/{$this->getDestinationSubdir()}_placeholder");
            $configPlaceholder   = '/placeholder/' . $isConfigPlaceholder;
            if ($isConfigPlaceholder && file_exists($baseDir . $configPlaceholder)) {
                $file = $configPlaceholder;
            }
            else {
                // replace file with skin or default skin placeholder
                $skinBaseDir     = Mage::getDesign()->getSkinBaseDir();
                $skinPlaceholder = "/images/catalog/product/placeholder/{$this->getDestinationSubdir()}.jpg";
                $file = $skinPlaceholder;
                if (file_exists($skinBaseDir . $file)) {
                    $baseDir = $skinBaseDir;
                }
                else {
                    $baseDir = Mage::getDesign()->getSkinBaseDir(array('_theme' => 'default'));
                }
            }
            $this->_isBaseFilePlaceholder = true;
        }

        $baseFile = $baseDir . $file;

        if ((!$file) || (!file_exists($baseFile))) {
            throw new Exception(Mage::helper('catalog')->__('Image file not found'));
        }

        $this->_baseFile = $baseFile;

        // build new filename (most important params)
        $path = array(
            Mage::getSingleton('catalog/product_media_config')->getBaseMediaPath(),
            'cache',
            Mage::app()->getStore()->getId(),
            $path[] = $this->getDestinationSubdir()
        );
        if((!empty($this->_width)) || (!empty($this->_height)))
            $path[] = "{$this->_width}x{$this->_height}";

        if(!empty($this->_cropDims))
            $path[] = "{$this->_cropDims[0]}x{$this->_cropDims[1]}";  
            
        // add misk params as a hash
        $miscParams = array(
                ($this->_keepAspectRatio  ? '' : 'non') . 'proportional',
                ($this->_keepFrame        ? '' : 'no')  . 'frame',
                ($this->_keepTransparency ? '' : 'no')  . 'transparency',
                ($this->_constrainOnly ? 'do' : 'not')  . 'constrainonly',
                $this->_rgbToString($this->_backgroundColor),
                'angle' . $this->_angle,
                'quality' . $this->_quality
        );

        // if has watermark add watermark params to hash
        if ($this->getWatermarkFile()) {
            $miscParams[] = $this->getWatermarkFile();
            $miscParams[] = $this->getWatermarkImageOpacity();
            $miscParams[] = $this->getWatermarkPosition();
            $miscParams[] = $this->getWatermarkWidth();
            $miscParams[] = $this->getWatermarkHeigth();
        }

        $path[] = md5(implode('_', $miscParams));

        // append prepared filename
        $this->_newFile = implode('/', $path) . $file; // the $file contains heading slash

        return $this;
    }
    
    
    /**
     * Convert array of 3 items (decimal r, g, b) to string of their hex values
     *
     * @param array $rgbArray
     * @return string
     */
    private function _rgbToString($rgbArray)
    {
        $result = array();
        foreach ($rgbArray as $value) {
            if (null === $value) {
                $result[] = 'null';
            }
            else {
                $result[] = sprintf('%02s', dechex($value));
            }
        }
        return implode($result);
    }

    
    /**
     * @return Mage_Catalog_Model_Product_Image
     */
    public function frontCrop(){
        $this->getImageProcessor()->frontCrop($this->_cropDims[0], $this->_cropDims[1]);
        return $this;
    }
    
     public function setCropDims($width, $height){
        $this->_cropDims = array($width, $height);
        return $this;
    }
}
Add all new getters, setters, variables and whatnots to the corresponding helper:
class Criography_ExtendedProductImage_Helper_Image extends Mage_Catalog_Helper_Image
{
	
		protected $_cropDims = array(0,0);
		protected $_scheduleCrop = false;

    /**
     * Reset all previos data
     */
    protected function _reset()
    {
        $this->_model = null;
        $this->_scheduleResize = false;
        $this->_scheduleRotate = false;
        $this->_scheduleCrop = false;
        $this->_angle = null;
        $this->_cropDims = array(0,0);
        $this->_watermark = null;
        $this->_watermarkPosition = null;
        $this->_watermarkSize = null;
        $this->_watermarkImageOpacity = null;
        $this->_product = null;
        $this->_imageFile = null;
        return $this;
    }
    
    
   
    
    public function frontCrop($width=0, $height=0){

    		$this->setCropDims($width, $height);
				$this->_getModel()->setCropDims($width, $height);
				$this->_scheduleCrop = true;
        
        return $this;
    }
    
    
    protected function setCropDims($width, $height){
        $this->_cropDims = array($width, $height);
        return $this;
    }

    
    protected function getCropDims(){
        return $this->_cropDims;
    }
    
    
    public function __toString()
    {
        try {
            if( $this->getImageFile() ) {
                $this->_getModel()->setBaseFile( $this->getImageFile() );
            } else {
                $this->_getModel()->setBaseFile( $this->getProduct()->getData($this->_getModel()->getDestinationSubdir()) );
            }

            if( $this->_getModel()->isCached() ) {
            	return $this->_getModel()->getUrl();
            } else {
            	
            	
            	
                if( $this->_scheduleRotate ) {
                    $this->_getModel()->rotate( $this->getAngle() );
                }

                if ($this->_scheduleResize) {
                    $this->_getModel()->resize();
                }
                
                if( $this->_scheduleCrop ) {
                		$this->_getModel()->frontCrop( $this->getCropDims() );
                }

                if( $this->getWatermark() ) {
                    $this->_getModel()->setWatermark($this->getWatermark());
                }

                $url = $this->_getModel()->saveFile()->getUrl();
            }
        } catch( Exception $e ) {
            $url = Mage::getDesign()->getSkinUrl($this->getPlaceholder());
        }
        return $url;
    }

}
and set up your usual XML files to configure and enable the module (first: config.xml, second: \app\etc\modules\Criography_ExtendedProductImage.xml).
<?xml version="1.0"?>
<config>
	<modules>
  	<Criography_ExtendedProductImage>
    	<version>0.1.0</version>
      <depends>
      	<Mage_Catalog />
      </depends>
    </Criography_ExtendedProductImage>
  </modules>

  <frontend>
  	<routers>
    	<extendedproductimage>
      	<use>standard</use>
        <args>
        	<module>Criography_ExtendedProductImage</module>
          <frontName>extendedproductimage</frontName>
        </args>
      </extendedproductimage>
   	</routers>
  </frontend>
  
  <global>
  	<helpers>
			<catalog>
				<rewrite>
					<image>Criography_ExtendedProductImage_Helper_Image</image>
				</rewrite>
			</catalog>
     </helpers> 
     <models>
			<catalog>
				<rewrite>
					<product_image>Criography_ExtendedProductImage_Model_Product_Image</product_image>
				</rewrite>
			</catalog>
     </models> 
   </global>
</config>
<?xml version="1.0"?>
<config>
    <modules>
        <Criography_ExtendedProductImage>
            <active>true</active>
            <codePool>local</codePool>
        </Criography_ExtendedProductImage>
    </modules>
</config>
Now here comes the phun part - grab \lib\Varien\Image.php and copy it to your \app\code\local\Varien\. Although you can't extend it, the local version will completely overwrite the original and will be used instead. Edit the file and just below the crop method add this new one:
public function frontCrop($width=0, $height=0){
     $this->_getAdapter()->frontCrop($width, $height);
}
Similarly copy the \lib\Varien\Image\Adapter\Gd2.php to \app\code\local\Varien\Image\Adapter\ and add this method somewhere below the original crop one:
public function frontCrop($width=0, $height=0){
   		
    if($width>10){
        		
        $canvasDims	=	array($width, $height==0 ? $width : $height);
        $canvas 		= imagecreatetruecolor($canvasDims[0], $canvasDims[1]);

				//calculate new dimensions
				if ($this->_imageSrcWidth/$this->_imageSrcHeight > $canvasDims[0]/$canvasDims[1]) {
					$targetDims[1]	= $canvasDims[1];
					$targetDims[0]	= round($this->_imageSrcWidth * $targetDims[1] / $this->_imageSrcHeight);
				} else {
					$targetDims[0]	= $canvasDims[0];
					$targetDims[1]	= round($this->_imageSrcHeight * $targetDims[0] / $this->_imageSrcWidth);
				}
						
				$posArray = array('src'=>array( -(($targetDims[0]-$canvasDims[0])/2), -(($targetDims[1]-$canvasDims[1])/2) ),
												'target'=>array( (($targetDims[0]-$canvasDims[0])/2),  (($targetDims[1]-$canvasDims[1])/2) )
									 );

				if ($this->_fileType == IMAGETYPE_PNG) $this->_saveAlpha($canvas);

		    imagecopyresampled(
		     	$canvas, 
		     	$this->_imageHandler, 
		     	$posArray['src'][0],
		     	$posArray['src'][1], 
		     	$posArray['target'][0], 
		     	$posArray['target'][1], 
		     	$targetDims[0], 
		     	$targetDims[1], 
		     	$this->_imageSrcWidth, 
		     	$this->_imageSrcHeight
		     );
		}else{
         return;
    }
				
    $this->_imageHandler = $canvas;
 		$this->refreshImageDimensions();
}
Finally you can use your new method the usual way with all the supported method chaining:
<img src="<?php echo $this->helper('catalog/image')->init($this->getProduct(), 'image', $_image->getFile())->frontCrop(100,90); ?>" />

As you can see it's not some fancy crop algorithm - it always starts from the centre and allows for single (square) or double (width, height) arguments but it is exactly what I was after. If you want to make something more sophisticated, feel free - I'd actually appreciate if you leave a comment or send me an email(: Oh, and it does work in 1.3.4 and 1.4rc!

  • Posted by covalic on 22 III 10 at 02:21
    Marku bardzo dobry tekst i wielkie dzięki za niego. Ale chyba nie do końca wiem co mam tu z tym zrobić. Byłem z tych 'leniwych' i pobrałem paczkę, wrzuciłem i... ścięło mi stronę przy pierwszej grafice ;)

    • Posted by criography on 22 III 10 at 10:27
      Hmm, a co ci PHP wyrzuca w logach? Może już używasz jakiegoś innego modułu, który też rozszerza Mage_Catalog_Model_Product_Image i się gryzą chłopaki?

  • Posted by covalic (@covalic) on 23 III 10 at 00:06
    To są moje zupełne początki, rzekłbym 'raczki' ;) i nie patrzyłem na logi. Poprosiłem kolegę o pomoc no i niby coś tam poprawił. Przepraszam za OT ale może mi podpowiesz jak się pozbyć tych zbędnych białych ramek u dołu i u góry zdjęcia?Generalnie chodzi mi o miniatury wyświetlane na stronie danej kategorii ale nie ukrywam że pozbyłbym się tego kompleksowo.

  • Posted by Nathan Petralia (@petralian) on 17 VI 10 at 06:02
    I used your method for my website www.mouyatea.com but since upgrade to 1.4.1 it seems to be stuck somewhere... do you have any idea?

    • Posted by Ayhan Binici on 8 VIII 10 at 19:55
      Just as Nathan, I experienced issues with making this to work on version 1.4.1. I managed to get it to work by changing the following line in the file app/code/local/Criography/ExtendedProductImage/Model/Product/Image.php: private function _rgbToString($rgbArray) to protected function _rgbToString($rgbArray) The signature must have been changed since 1.4. Thank you Marek for this wonderful contribution!


Your email address won’t be displayed but may be used to grab your gravatar.
Please no link dropping, no URLs or keywords as names. Do not spam and do not advertise!