JPEG сжатие картинки с альфа-каналом или SVG masks

    Привет, Хабр! Недавняя статья про сжатие в png-8 с сохранение полупрозрачности, напомнила мне об одной технике, которая позволяет применять на сайтах изображения с альфа-каналом, при этом используя алгоритм сжатия с потерями — JPEG, что позволяет существенно сократить их объём.


    Предыстория

    Некоторое время назад, когда об HTML5 мало кто знал, а flash был довольно распространён, мне пришлось изучить ActionScript и делать красивые «имиджевые» сайты. Обычно такой сайт был довольно насыщен картинками, и более того, для некоторых эффектов требовалось использовать фотографии с прозрачным фоном (например, когда товарная единица с тенью красиво вылетает откуда-нибудь по неоднородному фону). Но как известно JPEG не позволяет хранить в себе альфа-канал, а PNG — это формат сжатия без потерь, из-за чего размер такого фото получался мягко говоря большим. Во flash же возможно было сохранить полупрозрачные изображения с JPEG-компрессией, достигалось это тем, что альфа-канал сохранялся отдельно от изображения, получая два изображения, которые впоследствии сохранялись в JPEG.
    Позже верстая один из «обычных» html-сайтов, мне также потребовалось сохранить большой неоднородный полупрозрачный фон, но ни сохранение в png-32, ни в png-8 не давали приемлемый результат — png-8 выглядел отвратно, а png-32 весил слишком много. В поисках технологии, которая бы позволила сохранять изображения с альфа-каналом с потерей качества, я наткнулся на SVG Mask.

    Так для чего это нужно?

    Возьмём гипотетический пример — нужна нам вот-такая картинка с «дыркой» внутри:
    PNG-32 (PNG-32, 1870 Кбайт)

    Сжатие в 256ти-цветную палитру PNG-8 «убивает» изображение:
    PNG-8 (PNG-8, 305 Кбайт)

    Тогда как применяя flash мы получим swf скромного размера, при этом на глаз мало различимого от оригинала:
    flash (swf, 453 Кбайт)

    В принципе, если бы не сложности с удобством генерации swf, и не полной поддержки flash на всех устройствах, можно было бы на этом и закончить. Но есть более удобная HTML5 альтернатива!

    Суть метода

    Сам метод не новый и изредка встречается его описание на некоторых ресурсах. Но к сожалению информации о нём относительно мало, по крайней мере на хабре статьи не нашёл, что спешу поправить.
    Стандарт SVG, позволяет внедрять в себя растровые изображения, а также позволяет применять маски. Так почему же нам не реализовать всё так же, как и во flash?
    Отделяем альфа-канал и сохраняем его в отдельный файл:
    imagemask (JPEG, 165/46 Кбайт)

    Суммарный объём получился даже меньше, чем .swf — 211 Кбайт!

    Пишем SVG:
    out.svg
    <?xml version="1.0" encoding="utf-8" standalone="yes"?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 240" width="300px" height="240px" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
        <style>
            svg { -webkit-background-clip: text; }
        </style>
    	<defs>
    	  <mask id="mymask">
    		 <image xlink:href="mask.jpg" x="0" y="0" width="300px" height="240px" />
    	  </mask>
    	</defs>
    	<image xlink:href="image.jpg" mask="url(#mymask)" x="0" y="0" width="300px" height="240px" />
    </svg>
    

    Как вариант можно внедрить файлы прямо в svg (уменьшаем кол-во запросов), с помощью data: URI, правда в этом случае размер возрастёт в среднем на 33%, хотя это можно обойти с помощью gzip-сжатия. Пример.

    Как на счёт кроссбраузерности?

    В своё время я этот метод оставил на «светлое будущее», т. к. тогда поддержка SVG браузерами была не очень, да и заказчики с требованиями к ie6 ещё оставались, но время идёт, и сейчас уже можно применять этот метод в «боевых условиях».
    Про внедрение SVG в HTML уже были статьи на хабре: раз, два, поэтому особо подробно останавливаться на этом вопросе не буду. Кроме того вы возможно заметили, что прописал -webkit-background-clip: text;, это нужно для прозрачности внедряемого объекта в старых safari и chrome (в современных версиях этот баг уже закрыли).
    Но как же быть с ie7, ie8? Для этой «умирающей» аудитории мы можем предложить flash, а если и его нет, то, на худой конец, пусть грузят тяжёлый PNG-32 (или какую-нибудь заглушку).

    Решение

    Начнём с того как подключать SVG?
    Для обзора различных методов внедрения SVG, я сделал специальную демо-страничку.



    Кроме стандартного способа подключения через object/embed (ссылки на статьи выше), можно подключать SVG через <img> или CSS. Но как показала практика, через <img> SVG в firefox и chrome показывается только с внедрёнными через data: URI файлами, более того, некоторые устаревшие версии браузеров не поддерживают внедрённые таким способом SVG (например, firefox 3.6), кроме того, в этом случае невозможно управлять SVG с помощью JavaScript. Но если отбросить эти факты, то этот способ даже более предпочтителен для «статичных» картинок.
    Если поддержки SVG нет (ie6-8, android <3.0 и др.), то можно подменить его на flash или png. В поисках готового решения, я ничего не нашёл, поэтому написал свой велосипед (с использованием jQuery и SWFObject).

    В HTML подключаем библиотеки jQuery, swfobject и скрипт-фикс. Затем внедряем любым понравившимся методом svg-изображение. Для поддержки кроссбраузерности выбираем назначаем CSS-класс для внедрённых svg-элементов, и указываем через атрибуты data-altflash и/или data-altpng URL альтернативных источников.
    index.html
    <!DOCTYPE html>
    <html>
    	<head>
    		<title>SVG alpha</title>
    		<style>
    			body { margin: 0; padding: 0; font: 12px/1 Arial, sans-serif; text-align: center; background: #ccc url(bg.png) repeat fixed left top; }
    			h2 { margin-top: 2em; font-size: 120%; text-align: center; }
    			table { width: 800px; margin: 0 auto; }
    			table td { width: 300px; }
    			object,iframe,embed,img,.svg-bg { width: 300px; height: 240px; margin: 0; padding: 0; border: 0px none; }
    			object { -webkit-background-clip: text; }
    			.svg-bg { background: transparent none no-repeat scroll left top; }
    		</style>
    		<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    		<script src="https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js" type="text/javascript"></script>
    		<script type="text/javascript" src="svg.js"></script>
    		<script type="text/javascript">
    			$(function(){ $('.svg').svg(); }); // init
    		</script>
    	</head>
    	<body>
    		<table>
    			<tr>
    				<th>через object</th>
    				<th>через img (data:URI)</th>
    			</tr>
    			<tr>
    				<td><object class="svg" data="out.svg" type="image/svg+xml" data-altflash="flash.swf" data-altpng="png-32.png"></object></td>
    				<td><img class="svg" src="in.svg" alt="" data-altpng="png-32.png"/></td>
    			</tr>
    		</table>
    	</body>
    </html>
    

    svg.js (прошу сильно не критиковать за качество кода, он написан буквально на коленке):
    svg.js
    $.supportSvg = function()
    {
        return document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1");
    };
    
    $.svgReplaceImage = function()
    {
    	var img = new Image();
    	img.src = $(this).data('altpng');
    	$(this).replaceWith(img);
    };
    
    $.fn.svg = function()
    {
    	if($.supportSvg()) return;
    	
    	this.filter('[data-altpng]:not([data-altflash])').each($.svgReplaceImage);
    	var i = 0;
    	this.filter('[data-altflash]').each(function()
    	{
    		var obj = this;
    		this.id = 'svg-alt-'+(++i);
    		swfobject.embedSWF($(this).data('altflash'), this.id, $(this).width(), $(this).height(), '10.0.0', 'expressInstall.swf', false, {wmode: 'transparent'}, false,
    			function(e){ if(!e.success) $.svgReplaceImage.apply(obj); });
    	});
    };
    

    Итого

    Таким образом мы можем существенно сократить размер изображений. Данный метод протестирован на ie7-ie9, firefox 13, chrome 19, opera 12, Safari 5 (win), android 2.3. Просьба протестировать его в других браузерах. Спасибо за внимание!

    Ссылки

    Демо
    Архив (зеркало)

    PS. Не первый раз натыкаюсь на непонимание, что маска — это произвольное изображение, круговой градиент я использовал как пример, в реальности маска — JPEG-картинка, которая может быть какой угодно геометрической сложности!
    Share post

    Similar posts

    Comments 17

      +2
      Гениально!
        +3
        Ох ты ж как охренительно!!!
          –1
          Не знал, что jpeg может быть прозрачным.
          Только не понял, альфа-маска сохраняется как растровое или векторное изображение? По идее, она выглядит очень «геометрично», т.е. для построения нетрудно составить формулу, а значит, и описать векторно?
          Еще больше можно, теоретически, сэкономить, применив сжатие самого svg, т.к. xml формат сильно избыточен.

          Поправьте меня, если ошибаюсь.
            +1
            Обычное растровое изображение.
            Если маска простая — думаю, пару строк на js через canvas может ещё больше сократить объем (можно ещё масштабирование маски под исходный размер сделать).
              0
              Дело в том что
              1) Геометрическая маска это лишь пример. На деле она такая не всем нужна нужна.
              2) Расчет прозрачности ведется по пикселям — я думаю это очевидно. XML тут придется рендерить в растр для расчета в любом случае.Хотя теоретически и так рендерит это в растр для отображения на экране сам браузер или кто там… тут не знаю. см пунк 1. =)

              Мне лично кажется что на ява скрипте было бы куда удобнее, но возможно не быстрее…
                0
                Как хранить маску — решать вам. Хотите, используйте растровую маску, хотите — используйте всю мощь SVG для отрисовки маски (фигуры, полигоны и многое другое), хотите с помощью JavaScript — ничто вас здесь не ограничивает. Я показал лишь пример, что применять лучше — зависит от задач. Если у вас есть большая картинка с прозрачным фоном, на котором аккуратно вырезан человек — тут «геометрии» в маске мало. А если нужна просто аватарка вырезанная в форме «звезды», то конечно выгоднее использовать векторную маску.
                0
                Последняя Opera Next — не работают только примеры с «css background». Ubuntu.
                  0
                  Автор, а не пробовали ради любопытства все же IE через VML и что-то типа этого поковырять? В связи со временем суток мозг уже не соображает, поэтому сам осознать не могу — реально это или нет. :-)
                    0
                    На сколько я знаю, в VML нет аналогов SVG Mask, поэтому наложить растровый альфа-канал не получится, максимум что можно сделать — это использовать векторную маску. Поправьте меня если я не прав.
                    0
                    Ха-ха-ха! Internet Explorerединственный браузер, который смог отобразить картинку всеми способами. Ненавистники ИЕ, можете начинать скрипеть зубами. :-D

                    ФФ 13, Хром 19, Сафари 5 не одолели внешние файлы при CSS background и img; Опера 12 не одолела CSS background. А вот ИЕ 9 добросовестно отобразил все до единой картинки. Вроде, у всех браузеров автообновление не трогал, поэтому версии должны быть актуальные.

                    Есть, разумеется, один традиционный нюанс, который портит вся ляпоту — ИЕ 9 привязан к оси, до которой не все форточники обновились.
                      +3
                      Судя по консоли Firefox, внешние изображения не грузятся по соображениям «безопасности»:
                      Попытка нарушения системы безопасности: содержимое «http://paulzi.ru/habr/svg-alpha/out.svg» не имеет права загружать данные из paulzi.ru/habr/svg-alpha/mask.jpg.
                        0
                        Вы не в курсе, чем это может быть вызвано? Не гуглится чего-то.

                        И вообще, я не понимаю, как включение JPEG'а в SVG может быть уязвимостью, особенно если файлы находятся на одном сервере в одной директории.
                      +1
                      Можно упомянуть также для новых версий вебкита:
                      -webkit-mask (background)
                      -webkit-mask-attachment (background-attachment)
                      -webkit-mask-clip (background-clip)
                      -webkit-mask-origin (background-origin)
                      -webkit-mask-image (background-image)
                      -webkit-mask-repeat (background-repeat)
                      -webkit-mask-composite (background-composite)
                      -webkit-mask-box-image (border-image)
                        0
                        А можно и на canvas — если маска достаточно сложная.
                        banzalik.ru/transparent-jpg/
                          +1
                          Спасибо! Хотя не понял причём тут «сложная маска»? В качестве маски в статье используется изображение, и оно может быть каким угодно сложным. Кроме того для работы метода не требуется включённый JavaScript (по крайней мере в браузерах где есть поддержка SVG).
                            0
                            так тут в принципе любая маска будет работать. даже если не ошибаюсь картинкой.
                            btw всегда удивляло отсутствие в jpg альфаканала
                            0
                            хм… интересный подход. както боролись с аналогичной траблой.
                            в эту сторону както и не подумали.

                            Only users with full accounts can post comments. Log in, please.