Оптимизация изображений в UMI.CMS

Оптимизация изображений в UMI.CMS
Данная статья посвящена очень интересной теме, а именно, оптимизация изображений для UMI.CMS.

Проблемы которые нужно было решить:

1. Корректное именование изображений

2. Удаление из уменьшенного изображения пути типа - a5b0aeaa3fa7d6e58d75710c18673bd7ec6d5f6d

3. Вынос изображений на поддомен

Для чего я все это затеял, спросите вы, все очень просто, в UMI.CMS с изображениями не совсем все в порядке, с точки зрения SEO.

Работая у в студии Fresh IT, я попросил сео-специалистов сформировать требования к изображениям. Были выданы следующие пожелания:

1. Нужно правильно именовать изображения.

Вы скажете, зачем это нужно, стоит прописать у изображения атрибут alt и title и все будет ОК.

Это не так, для того, чтобы поисковые системы лучше сканировали ваши изображения, им нужно давать корректные имена.

2. Сделать путь к уменьшенным изображениями без a5b0aeaa3fa7d6e58d75710c18673bd7ec6d5f6d

Это нужно для того, чтобы поисковой системе было проще сканировать путь к изображениям.

3. Вынести изображения на поддомен

Это нужно для того, ускорить загрузку страницы.

Браузеры начинают загружать картинки не после того как загрузится весь html, а сразу как встречают их в html коде.

Для каждой картинки требуется соединение, каждое соединение грузит сервер. Что бы излишне не грузить сервер накладывается ограничение на сайт, к примеру может быть не более 10 соединений одновременно (сама страница, скрипты, стили), но сайт img.site.ru другой, и к нему можно ещё можно сделать 10 запросов, так получается что браузер грузит элементы страницы не в 10 потоков, а в 20.

Тем самым изображения UMI.CMS загружаются быстрее

Для реализации подобных требований был взять уже готовый макрос UMI.CMS makeThumbnailFull.

Итак поехали.

Чуть не забыл, данный пример рассматривает только XSLT-шаблонизатор. С TPL-шаблонизатором я более не работаю, да и вам не советую (если вы все мучаетесь с ним)

Также я использую стандартный шаблон demodizzy для реализации примеров. Путь к шаблону может отличаться от вашего.

Первое что нужно сделать, в создать в папке с вашим шаблоном вот такое дерево папок - \classes\modules\content\

В папке content два php файла class.php и permissions.php.

Далее в permissions.php добавить код:

<?php
$permissions = Array(
    'content' => Array('makeSubDomainImage')
);
?>

В файл class.php поместить вот этот код:

<?php
class content_custom extends def_module {
    /**
     * Функция для формирования уменьшенного и оригинального изображения на поддомене
     *
     * Параметры:
     * @subdomain - субдомен на котором должны храниться изображения - не забудьте создать в панели управления сайтом
     * // @save_folder - субдомен на котором должны храниться изображения - не забудьте создать в панели управления сайтом
     * @elem_id - указывает id объекта из которого нужно вытянуть инфомрацию
     * @field_name - указывает поле которое содержит изображение
     * @width - ширина уменьшенной копии
     * @height - высота уменьшенной копии
     * @crop - нужно ли обрезать изображение
     * @cropside - указывает положение рамки обрезания
     * @isLogo - указывает необходимость наложения водяного знака на изображение
     * @quality - указывает степень сжатия (качество) изображения
     *
     * Возвращаемые значения:
     * - домен
     * - поддомен
     * - уменьшенное изображение/оригинальное изображение:
     * --- высота/ширина уменьшенного изображения
     * --- размер
     * --- расширение
     * --- название файла
     * --- путь к файлу относительно DOCUMENT_ROOT
     * --- путь к файлу относительно поддомена сайта
     *
     * Дополнительно:
     * - Модернизация system makeThumbnailFull() - http://goo.gl/0kcJBs
     */
    function makeSubDomainImage($subdomain = false, $elem_id = false, $field_name = false,
                                $width = 'auto', $height = 'auto', $crop = true,
                                $cropside = 5, $isLogo = false, $quality = 100) {
        $arr = Array();

        if(!$subdomain) {
            $arr['error'] = '<![CDATA[ Укажите поддомен($subdomain) ]]>';

            return $arr;
        }

        if($elem_id) {
            $element = umiHierarchy::getInstance()->getElement($elem_id);

            if($element instanceOf umiHierarchyElement) {
                if($field_name) {

                } else {
                    $arr['error'] = '<![CDATA[ Укажите название поля, в котором содержится изображение($field_name) ]]>';

                    return $arr;
                }

                $image_elem = $element->getValue($field_name);
                $alt_name = $element->getAltName();

                if(is_object($image_elem)) {
                    $path = $image_elem->getFilepath();
                }
            }
        } else {
            $arr['error'] = '<![CDATA[ Не задан идентификатор объекта($elem_id) ]]>';

            return $arr;
        }

        $domain = getServer('HTTP_HOST');
        $thumbs_path = './' . $subdomain . '/thumbs/';
        $origin_path = './' . $subdomain . '/origin/';

        $isSharpen=true;

        $image = new umiImageFile($path);

        $file_name = $image->getFileName();
        $file_ext = strtolower($image->getExt());
        $file_ext = ($file_ext=='bmp'?'jpg':$file_ext);

        $allowedExts = Array('gif', 'jpeg', 'jpg', 'png', 'bmp');

        if(!in_array($file_ext, $allowedExts)) {
            $arr['error'] = '<![CDATA[ Не разрешенное расширение файла ]]>';

            return $arr;
        }

        // Если задан alt_name то нужно переименовать изображение
        if(iconv_strlen($alt_name) > 0) $file_name = $alt_name;
        else $file_name = substr($file_name, 0, (strlen($file_name) - (strlen($file_ext) + 1)) );

        /*Название папки в виде ширина_высота_вырезание_качество*/
        $thumbPath = $width . '_' . $height . '_' . $cropside . '_' . $quality;

        /*Определяет, является ли имя файла директорией*/
        if (!is_dir($thumbs_path . $thumbPath)) {
            /*Создаёт директорию*/
            mkdir($thumbs_path . $thumbPath, 0755, true);
        }

        /*Название уменьшенного изображения*/
        $file_name_new = $file_name . "." . $file_ext;

        /*Путь к уменьшенному изображению*/
        $path_new = $thumbs_path . $thumbPath . '/' . $file_name_new;

        if(!file_exists($path_new) || filemtime($path_new) < filemtime($path)) {
            if(file_exists($path_new)) {
                unlink($path_new);
            }

            $width_src = $image->getWidth();
            $height_src = $image->getHeight();

            if(!($width_src && $height_src)) {
                throw new coreException(getLabel('error-image-corrupted', null, $path));
            }

            if($height == "auto") {
                $real_height = (int) round($height_src * ($width / $width_src));
                //change
                $height=$real_height;
                $real_width = (int) $width;
            } else {
                if($width == "auto") {
                    $real_width = (int) round($width_src * ($height / $height_src));
                    //change
                    $width=$real_width;
                } else {
                    $real_width = (int) $width;
                }

                $real_height = (int) $height;
            }

            $offset_h=0;
            $offset_w=0;

            /*realloc: devision by zero fix*/
            if (!intval($width) || !intval($height)) {
                $crop = false;
            }

            if ($crop){
                $width_ratio = $width_src/$width;
                $height_ratio = $height_src/$height;

                if ($width_ratio > $height_ratio){
                    $offset_w = round(($width_src-$width*$height_ratio)/2);
                    $width_src = round($width*$height_ratio);
                } elseif ($width_ratio < $height_ratio){
                    $offset_h = round(($height_src-$height*$width_ratio)/2);
                    $height_src = round($height*$width_ratio);
                }

                if($cropside) {
                    switch ($cropside):
                        case 1:
                            $offset_w = 0;
                            $offset_h = 0;
                            break;
                        case 2:
                            $offset_h = 0;
                            break;
                        case 3:
                            $offset_w += $offset_w;
                            $offset_h = 0;
                            break;
                        case 4:
                            $offset_w = 0;
                            break;
                        case 5:
                            break;
                        case 6:
                            $offset_w += $offset_w;
                            break;
                        case 7:
                            $offset_w = 0;
                            $offset_h += $offset_h;
                            break;
                        case 8:
                            $offset_h += $offset_h;
                            break;
                        case 9:
                            $offset_w += $offset_w;
                            $offset_h += $offset_h;
                            break;
                    endswitch;
                }
            }

            $thumb = imagecreatetruecolor($real_width, $real_height);

            $source_array = $image->createImage($path);
            $source = $source_array['im'];

            if ($width*4 < $width_src && $height*4 < $height_src) {
                $_TMP=array();
                $_TMP['width'] = round($width*4);
                $_TMP['height'] = round($height*4);

                $_TMP['image'] = imagecreatetruecolor($_TMP['width'], $_TMP['height']);

                if ($file_ext == 'gif') {
                    $_TMP['image_white'] = imagecolorallocate($_TMP['image'], 255, 255, 255);
                    imagefill($_TMP['image'], 0, 0, $_TMP['image_white']);
                    imagecolortransparent($_TMP['image'], $_TMP['image_white']);
                    imagealphablending($source, TRUE);
                    imagealphablending($_TMP['image'], TRUE);
                } else {
                    imagealphablending($_TMP['image'], false);
                    imagesavealpha($_TMP['image'], true);
                }
                imagecopyresampled($_TMP['image'], $source, 0, 0, $offset_w, $offset_h, $_TMP['width'], $_TMP['height'], $width_src, $height_src);

                imageDestroy($source);

                $source = $_TMP['image'];
                $width_src = $_TMP['width'];
                $height_src = $_TMP['height'];

                $offset_w = 0;
                $offset_h = 0;
                unset($_TMP);
            }

            if ($file_ext == 'gif') {
                $thumb_white_color = imagecolorallocate($thumb, 255, 255, 255);
                imagefill($thumb, 0, 0, $thumb_white_color);
                imagecolortransparent($thumb, $thumb_white_color);
                imagealphablending($source, TRUE);
                imagealphablending($thumb, TRUE);
            } else {
                imagealphablending($thumb, false);
                imagesavealpha($thumb, true);
            }

            imagecopyresampled($thumb, $source, 0, 0, $offset_w, $offset_h, $width, $height, $width_src, $height_src);

            if($isSharpen) $thumb = makeThumbnailFullUnsharpMask($thumb,80,.5,3);

            switch($file_ext) {
                case 'gif':
                    $res = imagegif($thumb, $path_new);
                    break;
                case 'png':
                    $res = imagepng($thumb, $path_new);
                    break;
                default:
                    $res = imagejpeg($thumb, $path_new, $quality);
            }
            if(!$res) {
                throw new coreException(getLabel('label-errors-16008'));
            }

            imageDestroy($source);
            imageDestroy($thumb);

            if($isLogo) {
                umiImageFile::addWatermark($path_new);
            }
        }

        /*Создание нового уменьшенного изображения на поддомене*/
        $value = new umiImageFile($path_new);

        /*Техническая информация*/
        $arr['info']['nodes:elem_id'][] = $elem_id;
        $arr['info']['nodes:field_name'][] = $field_name;
        $arr['info']['nodes:path'][] = $path;
        $arr['info']['nodes:alt_name'][] = $alt_name;
        $arr['info']['nodes:width'][] = $width;
        $arr['info']['nodes:height'][] = $height;
        $arr['info']['nodes:crop'][] = $crop;
        $arr['info']['nodes:cropside'][] = $cropside;
        $arr['info']['nodes:isLogo'][] = $isLogo;
        $arr['info']['nodes:quality'][] = $quality;
        $arr['info']['nodes:domain'][] = $domain;
        $arr['info']['nodes:subdomain'][] = $subdomain . '.' .$domain;

        /*Уменьшенное изображение*/
        $arr['thumb_info']['nodes:width'][] = $value->getWidth();
        $arr['thumb_info']['nodes:height'][] = $value->getHeight();
        $arr['thumb_info']['nodes:size'][] = $value->getSize();
        $arr['thumb_info']['nodes:ext'][] = $value->getExt();
        $arr['thumb_info']['nodes:filename'][] = $value->getFileName();
        $arr['thumb_info']['nodes:filepath'][] = $value->getFilePath();
        $thumb_src = 'http://' . $subdomain . '.' .$domain . substr($value->getFilePath(true), (iconv_strlen($subdomain) +1 ));
        $arr['thumb_info']['nodes:src'][] = $thumb_src;

        /*Если задан alt_name то нужно заново обрабатывать увеличенное изображение*/
        /*Если нет, то нужно работать со старым изображением*/
        if(iconv_strlen($alt_name) > 0) {
            $file_name = $alt_name;
            /*Путь к увеличенному изображению*/
            $path_new_origin = $origin_path . '/' . $file_name . "." . $file_ext;

            if(!file_exists($path_new_origin) || filemtime($path_new_origin) < filemtime($path)) {
                /*Если файл существует*/
                if(file_exists($path_new_origin)) {
                    /*Удалить файл*/
                    unlink($path_new_origin);
                }

                /*Определяет, является ли имя файла директорией*/
                if (!is_dir($origin_path)) {
                    /*Создаёт директорию*/
                    mkdir($origin_path, 0755, true);
                }

                switch($file_ext) {
                    case 'gif':
                        $res = imagecreatefromgif($path);
                        imagegif($res, $path_new_origin);
                        break;
                    case 'png':
                        $res = imagecreatefrompng($path);
                        imagepng($res, $path_new_origin);
                        break;
                    default:
                        $res = imagecreatefromjpeg($path);
                        imagejpeg($res, $path_new_origin);
                }
                if(!$res) {
                    throw new coreException(getLabel('label-errors-16008'));
                }
                imagedestroy($res);

            }

            /*Создание нового изображения на подоменне*/
            $value_origin = new umiImageFile($path_new_origin);

            /*Большое изображение*/
            $arr['origin_info']['nodes:width'][] = $value_origin->getWidth();
            $arr['origin_info']['nodes:height'][] = $value_origin->getHeight();
            $arr['origin_info']['nodes:size'][] = $value_origin->getSize();
            $arr['origin_info']['nodes:ext'][] = $value_origin->getExt();
            $arr['origin_info']['nodes:filename'][] = $value_origin->getFileName();
            $arr['origin_info']['nodes:filepath'][] = $value_origin->getFilePath();
            $origin_src = 'http://' . $subdomain . '.' .$domain . substr($value_origin->getFilePath(true), (iconv_strlen($subdomain) +1 ));
            $arr['origin_info']['nodes:src'][] = $origin_src;
        }

        if(cmsController::getInstance()->getCurrentMode() == "admin") {
            $arr['src'] = str_replace("&", "&amp;", $arr['src']);
        }

        return $arr;
    }
}
?>

Теперь нужно добавить файл-шаблон, который будет обращаться к написанному макросу. Файл можете обозвать так, как вам удобно, у меня он называется makeSubDomainImage.xsl.
Далее разместите в удобную для вас папку(в вашем шаблоне). У меня он находиться в \templates\demodizzy\xslt\library\. Не забудьте подключить:

    <xsl:include href="makeSubDomainImage.xsl" />

Содержимое файла makeSubDomainImage.xsl

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xsl:stylesheet SYSTEM	"ulang://i18n/constants.dtd:file">

<xsl:stylesheet	version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:umi="http://www.umi-cms.ru/TR/umi">
	<xsl:template name="makeSubDomainImage">

		<xsl:param name="element_id" />
		<xsl:param name="field_name" />

        <xsl:param name="subdomain" />

		<xsl:param name="width">auto</xsl:param>
		<xsl:param name="height">auto</xsl:param>

        <xsl:param name="alt" />

        <xsl:param name="align" />
        <xsl:param name="id" />
        <xsl:param name="class" />
        <xsl:param name="link" />

        <xsl:param name="empty" /> <!-- Путь к пустому изображению -->

        <xsl:call-template name="makeSubDomainImage_thumbnail">
            <xsl:with-param name="element_id" select="$element_id" />
            <xsl:with-param name="field_name" select="$field_name" />

            <xsl:with-param name="subdomain" select="$subdomain" />

            <xsl:with-param name="width" select="$width" />
			<xsl:with-param name="height" select="$height" />

            <xsl:with-param name="alt" select="$alt" />
            <xsl:with-param name="align" select="$align" />
            <xsl:with-param name="id" select="$id" />
            <xsl:with-param name="class" select="$class" />
            <xsl:with-param name="link" select="$link" />

		</xsl:call-template>
	</xsl:template>

	<xsl:template name="makeSubDomainImage_thumbnail">
        <xsl:param name="element_id" />
        <xsl:param name="field_name" />

        <xsl:param name="subdomain" />

        <xsl:param name="width">auto</xsl:param>
		<xsl:param name="height">auto</xsl:param>

        <xsl:param name="alt" />
        <xsl:param name="align" />
		<xsl:param name="id" />
		<xsl:param name="class" />
		<xsl:param name="link" />

		<xsl:apply-templates select="document(concat('udata://content/makeSubDomainImage/',$subdomain,'/',$element_id,'/',$field_name,'/',$width,'/',$height))/udata" mode="makeSubDomainImage">
			<xsl:with-param name="element_id" select="$element_id" />
			<xsl:with-param name="field_name" select="$field_name" />

			<xsl:with-param name="alt" select="$alt" />
			<xsl:with-param name="align" select="$align" />
			<xsl:with-param name="id" select="$id" />
			<xsl:with-param name="class" select="$class" />
			<xsl:with-param name="link" select="$link" />

		</xsl:apply-templates>
	</xsl:template>

	<xsl:template match="udata[@method = 'makeSubDomainImage']" mode="makeSubDomainImage">
		<xsl:param name="element_id" />
		<xsl:param name="field_name" />
        <xsl:param name="alt" />
        <xsl:param name="align" />
        <xsl:param name="id" />
        <xsl:param name="class" />
        <xsl:param name="link" />

        <xsl:variable name="width" select="thumb_info/width" />
        <xsl:variable name="height" select="thumb_info/height" />

        <a href="{origin_info/src}" class="fancybox">

            <xsl:if test="$link">
                <xsl:attribute name="href">
                    <xsl:value-of select="$link" />
                </xsl:attribute>
            </xsl:if>

            <xsl:if test="$alt">
                <xsl:attribute name="title">
                    <xsl:value-of select="$alt" />
                </xsl:attribute>
            </xsl:if>

            <xsl:if test="$class">
                <xsl:attribute name="class">
                    <xsl:text>link-</xsl:text>
                    <xsl:value-of select="$class" />
                </xsl:attribute>
            </xsl:if>

            <img src="{thumb_info/src}" width="{$width}" height="{$height}" >
                <xsl:if test="$element_id">
                    <xsl:attribute name="umi:element_id">
                        <xsl:value-of select="$element_id" />
                    </xsl:attribute>

                    <xsl:attribute name="umi:field_name">
                        <xsl:value-of select="$field_name" />
                    </xsl:attribute>
                </xsl:if>

                <xsl:if test="$alt">
                    <xsl:attribute name="alt">
                        <xsl:value-of select="$alt" />
                    </xsl:attribute>

                    <xsl:attribute name="title">
                        <xsl:value-of select="$alt" />
                    </xsl:attribute>
                </xsl:if>

                <xsl:if test="$align">
                    <xsl:attribute name="align">
                        <xsl:value-of select="$align" />
                    </xsl:attribute>
                </xsl:if>

                <xsl:if test="$id">
                    <xsl:attribute name="id">
                        <xsl:value-of select="$id" />
                    </xsl:attribute>
                </xsl:if>
                <xsl:if test="$class">
                    <xsl:attribute name="class">
                        <xsl:text>img-</xsl:text>
                        <xsl:value-of select="$class" />
                    </xsl:attribute>
                </xsl:if>
            </img>
        </a>
	</xsl:template>
</xsl:stylesheet>

Со всем содержимым вроде разобрались, на всякий пожарный — вот ссылка для скачивания всего написанного выше.

Теперь давайте разберемся с работой данного макроса.
У него есть два режима работы. Первый режим работы — это страница каталога товаров. Второй — карточка товара.

Обращение к макросу должно осуществляться в файле category-view.xsl, вместо

<xsl:call-template name="catalog-thumbnail">
...
</xsl:call-template>

нужно вызвать:

<xsl:call-template name="makeSubDomainImage">
	<xsl:with-param name="subdomain">img</xsl:with-param>
	<xsl:with-param name="element_id" select="@id" />
	<xsl:with-param name="field_name">photo</xsl:with-param>
	<xsl:with-param name="width">154</xsl:with-param>
	<xsl:with-param name="height">110</xsl:with-param>
	<xsl:with-param name="alt" select="name" />
	<xsl:with-param name="class">image</xsl:with-param>
	<xsl:with-param name="link" select="@link" />
</xsl:call-template>
  • subdomain — субдомен на котором должны располагаться изображения. Его нужно не забыть настоит в панели управления вашим доменным именем или в хостинг аккаунте.
  • element_id — id объекта каталога
  • field_name — поля, в котором содержится изорражение
  • width/height — высота и ширина
  • alt — передается название объекта каталога, для alt и title атрибутов
  • link — ссылка на подробное описание товара

Второй вариант обращения происходит в карточке товара, в файле — object-view.xsl

<xsl:call-template name="makeSubDomainImage">
	<xsl:with-param name="subdomain">img</xsl:with-param>
	<xsl:with-param name="element_id" select="page/@id" />
	<xsl:with-param name="field_name">photo</xsl:with-param>
	<xsl:with-param name="width">281</xsl:with-param>
	<xsl:with-param name="alt" select="page/name" />
</xsl:call-template>

Параметры теже что и выше, кроме link, его передавать не нужно.
Теперь осталось настроить поддомен, для этого , понадобится зайти в ПУ вашим хостингом и у необходимого домена (mysite.ru) дописать в поле «псевдоним» поддомен «img.mysite.ru».
На этом все.

Старт дан, если вам будет интересно, то вы можете кастомизировать данный макрос под свои нужды, так как данный код не идеален.

Скачать можно по этой ссылке
Если у вас будут какие либо проблемы, пишите, будем разбираться 😉 Удачи!

===

UPD 09.04.2014. , спасибо Евгению за комментарий, забыл указать в статье.