Vector graphics
15 July 2010

К вопросу о кроссбраузерном использовании SVG

Для векторной графики в Интернете формат SVG — самое то. Во-первых, он поддерживает масштабирование любой степени. Во-вторых, можно обращаться к любым составляющим элементам такой картинки — адресовать их, стилизовать и скриптовать. В-третьих, за исключением совсем маленьких файлов, этот формат выигрывает по компактности перед любыми растровыми представлениями, особенно если применить gzip-сжатие. В-четвёртых, сие есть стандарт W3C.

Но как поместить SVG-картинку в HTML-документ?

Как вставить SVG в HTML



Известно, что есть три способа:

  • непосредственно в код (но — увы и ах! — IE этот способ не приемлет, да и остальные браузеры допускают лишь в XHTML-документе, что не всегда возможно);
  • в плавающий фрейм iframe (это я даже пробовать не стал, считая фреймы устаревшим злом);
  • через тег object (вот этим приёмом и займёмся).


Первый сюрприз нам преподносит… угадайте, какой браузер? Ну, конечно, это IE не даёт скриптовать встроенную посредством тега object SVG-картинку и заливает белым её прозрачный фон! К счастью, он предоставляет свой проприетарный тег embed для этих целей. Посредством условных комментариев и управлением стилями можно легко и валидно добиться, что нормальные браузеры будут отображать SVG через object-элемент, а IE — через embed. Но будет ли?

Второй сюрприз: не будет, пока мы не установим плагин — например, Adobe SVG Viewer. Правда, Adobe, вроде бы, прекратила его поддержку с 1 января 2009 г., но уже сделанного пока что должно хватить, да и, в конце концов, есть ещё RENESIS Player 1.0 for Internet Explorer и Ssrc SVG Plugin. Кроме того, как ни разобижена Microsoft за свой VML, с коим W3C её кинул в 1999 г., ввести нативную поддержку SVG её уламывают такие авторитеты как Google и Wikimedia Foundation. Обнадёживающая новость, что «Microsoft подала заявку на вступление в рабочую группу развития формата SVG в организации W3C» на Хабре уже обсуждалась. Так что тут есть все основания для оптимизма.

Далее. В описаниях тега embed во множестве мест в Интернете можно прочитать, что у него есть такой замечательный атрибут pluginspage, указывающий адрес, по коему, якобы, «будет направлен пользователь в том случае, если его браузер не поддерживает SVG-графику».

Не верьте! Это — третий сюрприз: в реальности IE игнорирует сей атрибут.

А чтобы заставить его работать, существует весьма малоизвестный (судя по тотальному отсутствию ссылок в тематических материалах в Интернете) хитрый способ с применением скриптов на JScript и VBScript. В принципе, ничего страшного, но об этом нужно знать.

Модернизация решения Adobe



Решение это, однако же, не вполне хорошо. Во-первых, оно не валидно, во-вторых, рассчитано на какие-то допотопные браузеры, коими уже никто не пользуется, но, в-третьих, не работает с современными браузерами (строго говоря, оно вообще не работает в том виде, в каком дано). Однако, основная идея решения верна, поэтому попытаемся сделать его работоспособным и современным.

Для начала, есть проблема вот с этой строчкой:

<script language="JavaScript"><!-- checkAndGetSVGViewer(); // --> </script>


Функция в ней не выполняется, если только мы не перенесём её вызов на новую строчку. Только в XHTML экранирование будет выполняться ещё сложнее, посему вынесем-ка мы вызов функции во внешний скрипт:

<script language="JavaScript" src="viewSVJ.js"></script>


Идём дальше. Нынешний код считает, что Firefox, Chrome и Safari для отображения SVG нуждаются в плагине, а Opera в этом отношении вообще безнадёжна. Однако, Opera имеет нативную поддержку SVG с версии 8.0 (19 апреля 2005 г.), Firefox — с версии 1.5 (30 ноября 2005 г.), Safari — с версии 3.0 (11 июня 2007 г.), а Chrome — с рождения (2 сентября 2008 г.). Фактически, все ухищрения нужны только для «Просмотрщика Интернета», сиречь Internet Explorer, у коего такая поддержка ожидается лишь в 9-й версии.

Тогда все три подключенных скрипта можно скрыть от прочих браузеров в условный комментарий (ну и насчёт языка перестаём притворяться, что это — JavaScript):

<!--[if IE]>
	<script language="JScript" src="svgcheck.js"></script>
	<script language="VBScript" src="svgcheck.vbs"></script>
	<script language="JScript" src="viewSVJ.js"></script>
<![endif]-->


Там, где непосредственно подключается SVG-картинка мы можем заключить рекомендованный код в такой же комментарий, а следом написать что-нибудь такое:

<!--[if !IE]>-->
	<object type="image/svg+xml" data="hello.svg" height="200" width="600"></object>
<!--<![endif]-->


В сумме получается довольно громоздкая конструкция, да ещё смешивающая представление с поведением. На самом деле можно просто написать:

<!--[if IE]>
	<embed src="hello.svg" height="100" width="100" type="image/svg-xml" pluginspage="http://www.adobe.com/svg/viewer/install/">
<![endif]-->
<!--[if !IE]>-->
	<object type="image/svg+xml" data="hello.svg" height="100" width="100"></object>
<!--<![endif]-->


А в подключаемом скрипте viewSVJ.js, если SVG-плагин всё-таки не установлен, будем перебирать элементы embed и заменять их на предупреждения со ссылками:

checkAndGetSVGViewer();
window.attachEvent(
	"onload",
	function(){
		if(window.svgInstalled)//если SVG-плагин установлен
			return;
		var embeds=document.getElementsByTagName("embed");
		for(var embedNumber=0, embedTypeAttr; embedNumber<embeds.length; embednumber++){
			embedtypeattr=embeds[embedNumber].attributes["type"];
			if(embedtypeattr="image/svg-xml" /*MSIE 5*/ || embedtypeattr.value="image/svg-xml")
				embeds[embednumber].outerhtml="<p>To view this page you need an SVG viewer. <a href=\""+getSVGInstallPage()+"\">Click here</a> for more information.</p>";
		}
	}
);


Но HTML-код всё равно выглядит чрезмерно раздутым. Ведь условный комментарий с элементом embed надо писать для каждой SVG-картинки, а ссылки на несколько скриптов (и тоже с условным комментарием) — для каждой содержащей их страницы! Конечно, существует шаблонизация, но… Вдобавок, эти скрипты замусоривают глобальное пространство имён, а лезть разбираться в их устаревший код не хочется.

Укротим сперва скрипты. Будем цеплять к странице один скрипт fixSVG.js, где для IE напишем свою ветку, в которой будем подгружать в динамически созданный плавающий фрейм (да, я фыркал по их адресу, но это же решение для браузера, который пока лучшего и не заслужил!) скрипты от «Эдоуби» (Adobe); выполнять с их помощью проверку на наличие установленного SVG-плагина; если он не установлен, подменять картинки на соответствующие уведомления со ссылкой; и, наконец, удалять этот фрейм (чистоты страницы ради). Детали реализации смотрим ниже.

Наконец, от тяжеловесной конструкции на месте каждой картинки можно избавиться, если пожертвовать браузером IE с установленным SVG-плагином, но отключенным JScript. Я склонен решительно пожертвовать. Всем известно, насколько этот браузер требует костылей для соблюдения стандартов, так что если человек отключает в нём скрипты, он молчаливо соглашается видеть Интернет «условно».

Можно писать просто:

<object type="image/svg+xml" data="hello.svg"></object>


Впрочем, если кому-то вышеописанный вариант дорог, как память, можно и так:

<object type="image/svg+xml" data="hello.svg">Для просмотра этой страницы нужен <a href="http://www.adobe.com/svg/viewer/install/">SVG viewer</a>.</object>


Затем для IE мы должны подменять элемент object на элемент embed, если SVG-плагин установлен, и на уведомление со ссылкой, если он ещё не установлен.

В заголовочной части страницы, как уже указано, пишем:

<script type="text/ecmascript" src="fixSVG.js"></script>


Файл fixSVG.js выглядит так (с использованием уже скриптового условного комментария):

/*@cc_on
	if(@_jscript_version<9)
		window.attachEvent(
			"onload",
			function(){
				var iframe=document.createElement("iframe");
				iframe.src="js/fixSVG_IE_5-8.html";
				document.body.appendChild(iframe);
			}
		);
@*/


Содержимое fixSVG_IE_5-8.html, подгружающееся в динамически созданный плавающий фрейм:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>Fix SVG for IE 5-8</title>
		<script type="text/jscript" src="svgcheck.js"></script>
		<script type="text/vbscript" src="svgcheck.vbs"></script>
		<script type="text/jscript" src="checkSVGViewer.js"></script>
	</head>
	<body></body>
</html>


Скрипты svgcheck.js и svgcheck.vbs — адоубивские, нетронутые. А fixSVG_IE_5-8.js — наш:

checkAndGetSVGViewer();//проверяем наличие SVG-плагина

if(window.svgInstalled){
	var objects=window.parent.document.getElementsByTagName("object");
	for(var objectNumber=objects.length, objectNode, objectTypeAttr, embedNode, codebase, attrNumber, attrNode; objectNumber--;){
		objectNode=objects[objectNumber];
		objectTypeAttr=objectNode.attributes["type"];
		if(objectTypeAttr=="image/svg+xml" || objectTypeAttr.value=="image/svg+xml"){
			embedNode=window.parent.document.createElement("embed");
			embedNode.setAttribute("type", "image/svg-xml");
			embedNode.setAttribute("pluginspage", "http://www.adobe.com/svg/viewer/install/");
			embedNode.setAttribute("wmode", "transparent");//обеспечиваем прозрачность фона
			codebase=objectNode.getAttribute("codebase");
			embedNode.setAttribute("src", ((null===codebase)?"":codebase)+objectNode.getAttribute("data"));
			for(attrNumber=objectNode.attributes.length; attrNumber--;){
				attrNode=objectNode.attributes[attrNumber];
				if(//сюрприз! IE не поддерживает indexOf!
					attrNode.name!="archive" &&
					attrNode.name!="classid" &&
					attrNode.name!="codebase" &&
					attrNode.name!="codetype" &&
					attrNode.name!="data" &&
					attrNode.name!="declare" &&
					attrNode.name!="standby" &&
					attrNode.name!="tabindex" &&
					attrNode.name!="type" &&
					attrNode.name!="usemap"
				)
					embedNode.setAttribute(attrNode.name, attrNode.value);
			}
			objectNode.parentNode.replaceChild(embedNode, objectNode);
		}
	}
}
else
	if(window.svgViewerAvailable){//для проформы (мы знаем, что для IE 5-8 такой плагин есть!)
		var message;
		switch(navigator.browserLanguage.substr(0,2)){
			case "ru":
				message="Для просмотра этой страницы нужен";
				break;
			case "en":
			default:
				message="To view this page you need an";
		}
		var objects=window.parent.document.getElementsByTagName("object");
		for(var objectNumber=objects.length, objectTypeAttr; objectNumber--;){
			objectTypeAttr=objects[objectNumber].attributes["type"];//objects[objectNumber].getAttribute("type")===null!
			if(objectTypeAttr=="image/svg+xml" || objectTypeAttr.value=="image/svg+xml")//тут учтён IE5
				objects[objectNumber].outerHTML="<p>"+message+" <a href=\""+getSVGInstallPage()+"\">SVG viewer</a>.</p>";
		}
	}

window.frameElement.parentNode.removeChild(window.frameElement);//удаляем плавающий фрейм, вызвавший этот скрипт


SVG-графика в Webkit-браузерах



Google Chrome и Safari, однако, обнаруживают две проблемы. Для начала, они не обеспечивают пропорционального масштабирования SVG-картинки в общем случае. 4-е версии для этого требовали, чтобы у элемента svg были заданы атрибуты width и height, а 5-е, напротив, чтобы они не были заданы! Но и при этом масштабирование получается не вполне удовлетворительное.

Вторая проблема серьёзней: если у вставленной через object SVG-картинки прозрачный фон, он заливается белым (это баг! причём очень старый, более чем двухлетней давности — и в 5-х версиях этих браузеров он не исправлен), как и в IE (но там мы можем просто добавить embed-элементу атрибут wmode со значением transparent).

Если использовать не object, а img, то всё отлично, но Firefox пока что такого способа вставки SVG-картинки не понимает. Поэтому пишем в вышеупомянутый файл fixSVG.js скрипт для Webkit-браузеров (смирившись с белым фоном в ненормальной ситуации отключенного Javascript), подменяющий в загруженном документе object с SVG-картинкой на img:

if(/AppleWebKit/.test(navigator.userAgent))
	window.addEventListener(
		"load",
		function(){
			var objects=document.getElementsByTagName("object");
			for(var objectNumber=objects.length, objectNode, codebase, imageNode, attrNumber, attrNode; objectNumber--;){
				objectNode=objects[objectNumber];
				if(objectNode.getAttribute("type")==="image/svg+xml"){
					imageNode=document.createElement("img");
					codebase=objectNode.getAttribute("codebase");
					imageNode.setAttribute("src", ((null===codebase)?"":codebase)+objectNode.getAttribute("data"));
					imageNode.setAttribute("alt", "SVG");//совсем без alt нельзя по стандарту HTML
					for(attrNumber=objectNode.attributes.length; attrNumber--;){
						attrNode=objectNode.attributes[attrNumber];
						if(-1===["declare", "classid", "codebase", "data", "type", "codetype", "archive", "standby", "tabindex"].indexOf(attrNode.name))
							imageNode.setAttribute(attrNode.name, attrNode.value);
					}
					objectNode.parentNode.replaceChild(imageNode, objectNode);
				}
			}
		},
		false
	);


Увы, но на вставленных таким образом картинках перестают работать скрипты, так что решение не универсально!

Вот так. Только помним при стилизации и скриптовании, что в Firefox, Opera и, очевидно, IE 9 будет элемент object, в Safari и Google Chrome (надеюсь, когда-нибудь везде) — img, а в IE 5—8 — embed.

Скриптование внедрённой SVG-картинки



В передовых браузерах получение доступа из HTML-документа к SVG-документу тривиально. По событию load нам оказывается доступен элемент object, на него тоже можно навесить событие load и в его обработчике мы замечательным образом получим искомое. Посредством jQuery это можно было бы сделать примерно так:

$(
	function(){
		$("object#SceletonObject").load(
			function(){
				var SVGDocument=$(this)[0].getSVGDocument();
				&hellip;
			}
		);
	}
);


Проблемы возникают в Internet Explorer. В нём мы можем после загрузки страницы получить элемент embed и у него даже уже доступен SVG-документ, но только &mdash; вот беда! &mdash; он совершенно пуст. Навесить на элемент embed событие load мне не удалось; похоже, оно вообще не поддерживается (впрочем, те из якобы поддерживаемых событий, что я пробовал, тоже не работают).

Кроме того, выяснилась ещё одна подлянка: содержимое таких элементов, как object и embed загружается асинхронно, в отдельном потоке. Это означает, как я понимаю, что, с одной стороны, пока не закончен парсинг основной страницы, элемент ещё недоступен для навешивания на него обработчика события, а когда он закончен &mdash; событие загрузки элемента может уже сработать и тогда обработчик навешивать уже бесполезно. Кажется, именно последний случай я периодически наблюдал в экспериментах, не улавливая никакой закономерности в глюке.

Да и по спецификации, кстати, событие load у элемента object отсутствует &mdash; оно есть в HTML только у элемента body. Так что то, что оно вообще где-то работает &mdash; это неправильно!

Выход найден следующий (не очень элегантный, но… будем ещё думать!): событие load пишем внутри SVG-кода у элемента svg, вызывая оттуда функцию HTML-документа. Как-нибудь так:

<svg xmlns="http://www.w3.org/2000/svg" onload="InitSVG(document);">


Ну, или, чтобы, уж, единообразно работало во всех браузерах:

<svg xmlns="http://www.w3.org/2000/svg" onload="if('InitSVG' in window) InitSVG(document); else parent.InitSVG(document);">


Или даже просто (поскольку parent в окне верхнего уровня указывает на само это окно):

<svg xmlns="http://www.w3.org/2000/svg" onload="parent.InitSVG(document);">


В функции InitSVG мы уже можем получить SVG-документ и из него &mdash; все его элементы. Лепота!

Только следует иметь в виду, что работать с SVG-элементами средствами jQuery не получится (может быть, есть какой-то обходной маневр, но мне его раскопать или изобрести пока не удалось и лично я посчитал, что овчинка выделки не стоит). Дело в том, что его функции ожидают получить от метода getElementsByTagName результат типа HTMLCollection, из коего можно отбирать элементы по индексу, как из обычного массива. А тут мы получим (везде, кроме Firefox) результат типа NodeList, из коего элемент можно будет выцепить только посредством особого метода item(индекс). Фреймворк на такое не рассчитывает и обламывается (во всяком случае, так было осенью, когда я с этим экспериментировал). Так что придётся работать непосредственно с DOM-методами, держа кроссбраузерность в уме. Сие напряжно, но возможно.

Два частных наблюдения по скриптованию SVG-документа. Во-первых, обнаружилось, что заданные в нём стили изменять скриптом не получается; всю динамику надо реализовывать через атрибуты (что, конечно, не очень правильно, если речь идёт об оформлении, к примеру, о цветовой заливке &mdash; fill). Во-вторых, если у SVG-элемента не стоит никакой заливки, то события mouseover и mouseout срабатывают только на рамке и вполне могут не успеть сработать вообще (к счастью, можно прописать атрибут fill один раз у элемента-предка и он пронаследуется).

Забавно, что этот способ перестал работать локально в Chrome 5.0 из-за каких-то его заморочек с безопасностью. Совсем не забавно, что, как уже указано, в Webkit-браузерах он вообще не работает, если мы используем тег img.

Как отдавать с сервера SVGZ



SVG-графику рекомендуется подвергать gzip-сжатию. Оно и правильно, размер файла тогда может стать в три-четыре раза меньше. Если кому-то, как и мне, не хочется возиться с запускаемой из командной строки утилитой gzip, он может воспользоваться для этой цели не менее бесплатным, но наделённым графическим интерфейсом архиватором 7-Zip (в настройках архивирования обязательно выбрать метод Gzip!).

Заархивировали, сменив расширение с образующегося автоматически .svg.gz на .svgz, после чего с изумлением убеждаемся, что получившийся файл адекватно воспринимают только Opera и Internet Explorer, а Firefox, Safari и Chrome воротят нос, ругаясь на синтаксис XML. Ну, конечно, какой там XML, пока не разархивируешь? А почему, собственно, они не разархивируют?

Оказывается, виноват Apache. До сих пор (версия 2.2.12) он, в конфигурации по умолчанию, посылает браузеру требующийся для SVG-формата заголовок Content-Type: image/svg+xml, но не требующийся для его gzip-ованной версии заголовок Content-Encoding: gzip. Посему лезем исправлять в конфигурацию сервера или просто пишем в файл .htaccess строчку «AddEncoding gzip .svgz».

Встречался мне ещё совет дописывать туда же (ради Firefox) следующее:

<FilesMatch \.svgz$>
	<IfModule mod_gzip.c>
		mod_gzip_on No
	</IfModule>
</FilesMatch>


Возможно, это имеет смысл, если на сервере настроено автоматическое gzip-ование отдаваемых файлов; не знаю.

Недавно я имел по этому вопросу смешные бодания с техподдержкой своего хостера, кои продолжались месяц. Я так понял, что перед Apache там стоит фронт-ендом nginx, что неким образом ограничивает возможность использования директив Apache. Конкретнее, директива AddType срабатывает, а AddEncoding — нет. Разумеется, раздел «Помощь» на сайте провайдера не содержит про эту закавыку ни слова и даже вообще ни единого упоминания про nginx. Техподдержка упорно не хотела мне внимать и выдавать свои тайны, потчуя меня безумными отписками: «Заголовок не отдается, поскольку на сервере не установлен необходимый для
работы директивы AddEncoding модуль Apache Mod_gzip», «За сжатие отвечает nginx, остальные директивы Apache поддерживаются в полном объёме» (неправда, кстати), «Приносим извинения. Передавать формат svgz можно лишь при наличии модуля mod_deflate, который, к сожалению, отсутствует на серверах, используемой Вами услуги» (и тут же предложение купить впятеро более дорогой план!). В конце концов, мне так и не объяснили, в чём у них закавыка, но сдались и стали отдавать правильный заголовок сами.

+20
39.4k 99
Comments 41