Первый и, насколько я знаю, на настоящий момент единственный оптимизатор SVG-файлов

Первый и, насколько я знаю, на настоящий момент единственный оптимизатор 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 год