Cropping with Magento

Update:

I haven't been working on Magento for a while now, but few of you told me that the module doesn't work anymore. The fix is very simple—always make sure you use current build Gd2.php and Image.php files as a base and paste apropriate chunks of code to them. Also, follow error images as they are quite selfexplanatory (e.g: one of the methods should be protected instead of private). I just updated the package but I suggest you do follow my instructions and code it yourself, so you know how to maintain it in the future!


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!

<?php
    
    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:

<?php

    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" encoding="utf-8"?>
  <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!

  • TeeJay

    I cant get it to work, I copied your package into the the app folder, but it will never use the new helper. do I need to activate it ? I am a complete magento noob so please excuse this probably silly question.

  • teejay

    uhh I figured it out I forgot the xml file. It works now … sort of. Cropping squares doesnt seem to work properly. I will write a full report once I have more time

  • criography

    To be fair, I wrote this article for rather old versions of Magento, nearly 3 years ago. I imagine the code might have changed since then. I may take a look at this this week if I find a sec, but I honestly hoped Magento would have added this functionality by now…

  • cuz83

    I copied the package into magento 1.7.2 and it stops the page from loading. I got quite excited seeing your solution as it sisnt require using phpthumb etc… Reckon it would be asy to get to work on 1.7?

  • The front calls I believe are different in the latest Magento versions, something like this:

    echo $this->helper(‘catalog/image’)->init($_product, ‘small_image’)->constrainOnly(TRUE)->keepAspectRatio(TRUE)->keepFrame(FALSE)->resize(206,206);

    How does one get your frontCrop() function to work in this new scheme of things?

  • Yogendra Mishra

    I have placed this line of code in list.phtml but its not working :
    <img src="helper(‘catalog/image’)->init($this->getProduct(), ‘image’, $_image->getFile())->frontCrop(100, 90); ?>”/>
    Why are we using$_image->getFile() as we aren’t defining the $_image variable. Also init function is not accepting the third parameter. Let me know if we can use it without using the third parameter.
    Thanks