How A Product Image Is Generated In Magento

Degi Kwag

For a commerce site, it’s normal to assume all the products have to serve product images in  various forms and sizes when requested in a different context. Be it small size, swatch_image, thumbnail, watermark, etc. Magento doesn’t sit and watch a store owner tediously upload each individual file for various size of same product image required. Magento handles by itself complex process of product image generation using GD library and ImageMagick. Since Magento has already put them well in an abstract design, you really need not bother to implement those libraries yourself.
 

Image generation process flow

However, you still need to know which classes are in charge of generating images or their blocks to place them within when an image is requested for a product in various contexts just in case of the process not working as expected.

Magento\Catalog\Model\Product\Image
Magento\Catalog\Model\Product\Image\Cache
Magento\Catalog\Helper\Image
Magento\Catalog\Block\Product\ImageBuilder

These 4 classes are what works hard when a catalog needs a product image and request as such. They all reside in the Catalog module. Normally the gateway for image request is

Magento\Catalog\Block\Product\AbstractProduct::getImage

Gateway for the process

    /**
     * Retrieve product image
     *
     * @param \Magento\Catalog\Model\Product $product
     * @param string $imageId
     * @param array $attributes
     * @return \Magento\Catalog\Block\Product\Image
     */
    public function getImage($product, $imageId, $attributes = [])
    {
        return $this->imageBuilder->setProduct($product)
            ->setImageId($imageId)
            ->setAttributes($attributes)
            ->create();
    }

What this snippet of code does is literally using the ImageBuilder to create an image tailored for a product in a certain context. You can understand what it does just by reading the code. Particularly the setImageId method configures in which context the image is going to be used. For example, it can be category_page_grid or category_page_list which informs the ImageBuilder of the target image context so that they are dressed as fit in the layout as possible.

Image builder class

Then the ImageBuilder creates an Image block because ImageBuilder class is basically a block class that returns an Image block class as follows :
 

/**
 * Create image block
 *
 * @return \Magento\Catalog\Block\Product\Image
 */
public function create()
{
    /** @var \Magento\Catalog\Helper\Image $helper */
    $helper = $this->helperFactory->create()
        ->init($this->product, $this->imageId);
    $template = $helper->getFrame()
        ? 'Magento_Catalog::product/image.phtml'
        : 'Magento_Catalog::product/image_with_borders.phtml';
    $imagesize = $helper->getResizedImageInfo();
    $data = [
        'data' => [
            'template' => $template,
            'image_url' => $helper->getUrl(),
            'width' => $helper->getWidth(),
            'height' => $helper->getHeight(),
            'label' => $helper->getLabel(),
            'ratio' =>  $this->getRatio($helper),
            'custom_attributes' => $this->getCustomAttributes(),
            'resized_image_width' => !empty($imagesize[0]) ? $imagesize[0]
 : $helper->getWidth(),
            'resized_image_height' => !empty($imagesize[1]) ? $imagesize[1]
 : $helper->getHeight(),
        ],
    ];
    return $this->imageFactory->create($data);
}

Basically this snippet does creating an image helper to configure all the details of the image that needs creation. You may find this helper class is where all the magics happen if you take a look into the class’s methods. When the helper is created, it also chains the init method to prepare the whole configuration for the image. Then, template is set as per the configuration for its frame, then it configures the size for the image by using getResizedImageInfo method.

ImageHelper initialization

Looking further into Magento\Catalog\Helper\Image::init method as follows :
 

    /**
     * Initialize Helper to work with Image
     *
     * @param \Magento\Catalog\Model\Product $product
     * @param string $imageId
     * @param array $attributes
     * @return $this
     */
    public function init($product, $imageId, $attributes = [])
    {
        $this->_reset();
        $this->attributes = array_merge(
            $this->getConfigView()->getMediaAttributes('Magento_Catalog',
 self::MEDIA_TYPE_CONFIG_NODE, $imageId),
            $attributes
        );
        $this->setProduct($product);
        $this->setImageProperties();
        $this->setWatermarkProperties();
        return $this;
    }

The protected _reset method literally reset all the data for the Image model configured previously including rotation, angle, watermark, position, size and other attributes. Next, it takes attributes from view.xml configuration and from method argument then merge them to prepare image properties.

view.xml for media type attributes

What is supposed to exist in view.xml for a theme with respect to image generation?

<view xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd">
    <media>
        <images module="Magento_Catalog">
            <image id="category_page_grid" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
        </images>
    </media>
</view>

For a theme to work properly, there must be an image node defined in view.xml with id attribute identical to the ones used in getImage method across the whole theme (in this case, it’s category_page_grid). For a demonstration, they are defined in /etc/view.xml of default themes out of the box, so if you expand your theme based on Magento default blank theme or luma theme, you really don’t need to elaborate this. In contrast, when you work on starting a new theme from scratch without inheriting a parent theme whose view.xml is already robustly defined, you must create view.xml with all the definitions that are used across the whole Magento templates in your theme. Otherwise you would see strange errors with a trace as follows :

Magento\Framework\View\Asset\File->getSourceFile() : Unable to resolve the source file for ‘<Area>/<Vendor>/<Theme>/<Locale>/Magento_Catalog/images/product/placeholder/.jpg’

which is almost useless to track the real issue where Magento\Framework\Config\View::getMediaAttributes method failed to fetch media attributes required in view.xml to process a certain imageId passed along with getImage method.  I wish Magento core team could provide more robust solution to this with fallback configurations for default media types. Or, at least, a proper conditional statement to point to the caveat scenario where merged attributes for image are empty so that debug trace can give a good hint for addressing the real issue.

Wrapping-up the initialization

Going forward, product is set for the model to reference, and then image properties are configured in setImageProperties method where the type of image(i.e. thumbnail, small size or normal size, swatch and so on), width, height, ratio constrain, transparency and background color is set by attributes merged above. Finally it configures the watermark whose properties include opacity, position and size, then returns the Image model class.
So, everything is up and ready now to create the image passing data configured so far to ImageFactory class in the Magento\Catalog\Block\Product\ImageBuilder::create method.

Template implementation

Now we’ve wrapped up how an image is processed in Magento, to finish this post off, let’s see a real example of how the method is evoked in the catalog/product/list.phtml as follows :

    if ($block->getMode() == 'grid') {
        $viewMode = 'grid';
        $image = 'category_page_grid';  //// Degi :
This is going to be $imageId argument
        $showDescription = false;
        $templateType =
\Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW;
    } else {
        $viewMode = 'list';
        $image = 'category_page_list';
        $showDescription = true;
        $templateType =
\Magento\Catalog\Block\Product\ReviewRendererInterface::FULL_VIEW;
    }
    /**
     * Position for actions regarding image size changing in vde if needed
     */
    $pos = $block->getPositioned();
    ?>
    <div class="products wrapper <?php /* @escapeNotVerified */ echo
$viewMode; ?> products-<?php /* @escapeNotVerified */ echo $viewMode; ?>">
        <?php $iterator = 1; ?>
        <ol class="products list items product-items">
            <?php /** @var $_product \Magento\Catalog\Model\Product */ ?>
            <?php foreach ($_productCollection as $_product): ?>
            <?php /* @escapeNotVerified */ echo($iterator++ == 1) ?
'<li class="item product product-item">' :
'</li><li class="item product product-item">' ?>
            <div class="product-item-info" data-container="product-grid">
                <?php
                $productImage = $block->getImage($_product, $image); ////
Degi : This is where the image process starts
                if ($pos != null) {
                    $position = ' style="left:' . $productImage->getWidth()
 . 'px;' //// Degi : processed image has several useful methods to use
                        . 'top:' . $productImage->getHeight() . 'px;"';
                }
                ?>
                <?php // Product Image ?>
                <a href="<?php /* @escapeNotVerified */ echo
$_product->getProductUrl() ?>"
 class="product photo product-item-photo" tabindex="-1">
                    <?php echo $productImage->toHtml(); //// Degi :
This is how the processed image block is HTMLed ?>
                </a>

Look for my comments in the code above that starts with “//// Degi : …” to get the idea of how it’s implemented.
 
Thanks for the reading, please leave any comment for your thoughts!

Leave a Comment

Share this post

Related Posts

See all posts