Первый и, насколько я знаю, на настоящий момент единственный оптимизатор SVG-файлов
Первый и, насколько я знаю, на настоящий момент единственный оптимизатор
<?php
// Written by Evgeny Stepanischev aka BOLK (http://bolknote.ru), 2009 (Public Domain License)
$_ENV = getopt('DCIFr::f::');
if (!is_numeric($_ENV['r']) || $_ENV['r'] < 0) $_ENV['r'] = 1;
if (!isset($_ENV['f']) || $_ENV['f'] === false) {
$help = file_get_contents(__FILE__, null, null, __COMPILER_HALT_OFFSET__);
$trans = array(
'»' => '"',
'«' => '"',
'—' => '--',
);
if (strtolower(substr(PHP_OS, 0, 3)) == 'win')
$help = iconv('utf-8', 'cp866//IGNORE', strtr($help, $trans));
die($help);
}
// стили по умолчанию, их можно удалить без вреда
$toKill = array(
"opacity:1(?:\.0+)?",
"color:#(?:000000|000|black)",
"fill\-opacity:1(?:\.0+)?",
"fill\-rule:nonzero",
"stroke\-width:1(?:\.0+)?(?:px)?",
"stroke\-linecap:butt",
"stroke\-linejoin:miter",
"stroke\-miterlimit:4(?:\.0+)?",
"stroke\-dasharray:none",
"stroke\-dashoffset:0(?:\.0+)?",
"stroke\-opacity:1(?:\.0+)?",
"marker:none",
"marker\-(?:start|end|mid):none",
"visibility:visible",
"display:inline",
"overflow:visible",
"enable\-background:accumulate"
);
$doc = new DOMDocument;
$doc->preserveWhiteSpace = $doc->formatOutput = isset($_ENV['F']);
$doc->Load($_ENV['f'], LIBXML_NOBLANKS | LIBXML_NONET | LIBXML_NSCLEAN);
$doc->normalizeDocument();
// тег metadata не несёт пользы для отображения
killNodes($doc->getElementsByTagName('metadata'));
// тег perspective тоже нужен только Inkscape
killNodes($doc->getElementsByTagNameNS('http://www.inkscape.org/namespaces/inkscape', 'perspective'));
// тег namedview пространства sodipodi нам бесполезен
killNodes($doc->getElementsByTagNameNS('http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 'namedview'));
// если defs оказался пустым, то удаляем и его
$toremove = array();
foreach ($doc->getElementsByTagName('defs') as $entry) {
if (!$entry->hasChildNodes()) {
$toremove[] = $entry;
}
}
killNodes($toremove);
// подготавливаем данные о стилях — переводим из удобного формата в регулярку
$toKill = '/^' . implode('|', $toKill) . '$/sui';
// раз уж мы сейчас пройдём по всем тегам, то соберём сразу все ID, которые присвоены тегам
// и все ID, которые реально используются в inline-стилях
$ids = $idsinline = array();
// это массив объектов тегов, которые можно впоследствии удалить
$toremove = array();
// у всех тегов оптимизирует атрибут style
foreach ($doc->getElementsByTagName('*') as $entry) {
// эти атрибуты добавляет программа Inkspace, нам они так же не нужны
// http://wiki.inkscape.org/wiki/index.php/Inkscape_SVG_vs._plain_SVG
removeUselessAttr($entry, array(
'collect',
'label',
'groupmode',
'type',
'nodetypes',
'tiled-clone-of',
'tile-cx',
'tile-cy',
'tile-h',
'tile-w',
'connector-avoid',
'connector-type',
'connector-start',
'connector-end',
));
if ($entry->tagName == 'svg') {
removeUselessAttr($entry, array(
'output_extension',
'docname',
'docbase',
'version',
'modified',
));
}
elseif ($entry->tagName == 'path') {
removeUselessAttr($entry, array(
'original',
'radius',
'type',
'cx', 'cy', 'rx', 'ry',
'argument', 'expansion', 'revolution', 't0', 'flatsided', 'randomized',
'rounded', 'arg1', 'arg2', 'sides',
));
if ($entry->hasAttribute('d')) {
$attr = trim($entry->getAttribute('d'));
// теги path с пустым d ничего не отображают
if ($attr == '') {
$toremove[] = $entry;
continue;
} else {
// снижаем точность чисел и записываем их более компактно
$entry->setAttribute('d', compactD($attr));
}
}
}
if ($entry->hasAttribute('style')) {
$nondef = array();
$killFill = $killStroke = false;
$styles = preg_split('!\s*;\s*!u', $entry->getAttribute('style'), -1, PREG_SPLIT_NO_EMPTY);
// если у fill или strike дефолтные значения, то помечаем на удаление соответствующие стили
foreach ($styles as $style) {
if ($killFill && $killStroke) break;
if (!$killFill && preg_match('/^fill\s*:\s*none$/siu', $style)) $killFill = true;
if (!$killStroke && preg_match('/^stroke\s*:\s*none$/siu', $style)) $killStroke = true;
}
// удаляем дефолтные и бесполезные стили
foreach ($styles as $style) {
// удаляем пробельные символы между именем и значением
$style = preg_replace('/^([^:]+)\s*:\s* /su', '\\1:', $style);
if (
(!$killStroke || strpos($style, 'stroke-') !== 0) &&
(!$killFill || strpos($style, 'fill-') !== 0) &&
!preg_match($toKill, $style) ||
isset($_ENV['D'])
) {
// пишем цвет в более компактной форме
if (!isset($_ENV['C']))
$style = preg_replace(
'/^(fill|stroke|stop-color|flood-color|lighting-color):#(.)\\2(.)\\3(.)\\4/su',
'\\1:#\\2\\3\\4',
$style
);
$nondef[] = $style;
if (preg_match('!^[^:]+:url\(#([^\)]+)\)!sui', $style, $m)) $idsinline[$m[1]] = null;
}
}
// заодно исчезнут пробельные символы между стилевыми правилами
$entry->setAttribute('style', implode(';', $nondef));
}
if ($entry->hasAttribute('id')) {
$ids[$entry->getAttribute('id')] = $entry;
}
// у номерных свойств урезаем незначащие нули
foreach ($entry->attributes as $attr) {
if ($attr->name <> 'id' && $attr->name <> 'd') {
if (is_numeric($attr->value)) {
// числовые атрибуты могут не содержать нуля перед точкой
$entry->setAttribute($attr->name, ltrim((float) $attr->value, '0'));
} else {
$entry->setAttribute($attr->name, preg_replace('/\b(?<!\#)\d+\.\d+\b/sue', '(float) \\0', $attr->value));
}
}
}
if ($entry->hasAttributeNS('http://www.w3.org/1999/xlink', 'href')) {
$idsinline[substr($entry->getAttributeNS('http://www.w3.org/1999/xlink', 'href'), 1)] = null;
}
if ($entry->hasAttribute('clip-path')) {
$value = $entry->getAttribute('clip-path');
if (preg_match('/^url\(#([^\)]+)\)$/isu', $value, $m)) {
$idsinline[$m[1]] = null;
}
}
}
// внутри цикла перебора не удаётся удалить ноду, это известное поведение модуля DOM
killNodes($toremove);
// мы можем проделать эту операцию удаления лишних ID, только если в SVG нет скриптов и таблиц стилей —
// ID могут быть использованы там
if (!isset($_ENV['I']) && !$doc->getElementsByTagName('script')->length && !$doc->getElementsByTagName('style')->length) {
// удаляем все ID, которые не были использованы как ссылки
foreach (array_diff_key($ids, $idsinline) as $entry) if ($entry) {
$entry->removeAttribute('id');
}
}
// и вычищаем все ненужные пространства имён
foreach ($doc->getElementsByTagName('svg') as $entry) {
$uselessNS = array(
'dc' => 'http://purl.org/dc/elements/1.1/',
'cc' => 'http://creativecommons.org/ns#',
'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'sodipodi' => 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
'inkscape' => 'http://www.inkscape.org/namespaces/inkscape',
);
foreach ($uselessNS as $ns => $url)
$entry->removeAttributeNS($url, $ns);
}
// удаляем комментарии и виндовые переводы строк
echo preg_replace('/\n?<!--.*?-->\n?/su', '', str_replace("\r", '', $doc->saveXML()));
// простые регулярные выражения для более компактной записи атрибута «d» для тега path
function compactD($str)
{
$patterns = array(
'/\d+\.\d{2,}/e',
'/(\D)\s+(\d)/s',
'/(\d+)\s+(\D+)/s',
'/(\d+)\s+\-(\d+)/s',
);
$replacements = array(
'round(\\0, $_ENV["r"])',
'$1$2',
'$1$2',
'$1$2',
);
return preg_replace($patterns, $replacements, $str);
}
// удаление нод
function killNodes($nodes)
{
foreach ($nodes as $entry)
$entry->parentNode->removeChild($entry);
}
// удаление атрибутов ненужных пространств
function removeUselessAttr($entry, array $names) {
foreach ($names as $name) {
$entry->removeAttributeNS('http://www.inkscape.org/namespaces/inkscape', $name);
$entry->removeAttributeNS('http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', $name);
}
}
__halt_compiler();
Использование: svgcrush [ОПЦИИ] -fФАЙЛ > РЕЗУЛЬТИРУЮЩИЙ_ФАЙЛ
Утилита для оптимизации размера файла SVG.
-fФАЙЛ файл, который будет обработан
-r1,-r2,..,-r9 точность чисел в атрибуте «d» тега «path» (по умолчанию — 1)
-D не удалять атрибуты со значениями по умолчанию (по умолчанию — включено)
-C не оптимизировать значения цветов (по умолчанию — оптимизировать)
-I не удалять неиспользуемые ID (по умолчанию — удалить)
-F форматировать XML (по умолчанию — нет)
Начальные исследования и часть методов оптимизации — Jos Hirth (http://kaioa.com/node/35), февраль 2007
Автор — Евгений Степанищев (http://bolknote.ru), июль 2009 год