CPU3D.comКомпьютерная графикаИстория → Экспериментальное сжатие SVG

Экспериментальное сжатие SVG

сжатие файлов

Вступление

Файлы SVG можно оптимизировать на предмет трех вещей: размер файла, скорость визуализации и редактирование. В большинстве случаев необходима оптимизация для редактирования — в особенности это касается сложных рисунков. Ближе к концу вам может захотеться достичь и две другие цели.

Оптимизация скорости визуализации довольно интересна в отношении устройств с ограниченными вычислительными возможностями вроде мобильных телефонов или КПК. Здесь следует обращать внимание на число узлов кривых, клоны, обтравочные контуры, маски, градиенты и даже такие простые вещи как обводка. Специалисты Nokia опубликовали достаточно подробное руководство (PDF) по этой теме.

Оптимизация размера файла влияет и на скорость визуализации. Использовать как можно меньше узлов, объединять контуры... штуки вроде того. Тем не менее, тут полностью игнорируется overdraw, и вы кругом используете клоны/обтравочные контуры/маски/обводки/градиенты. Разумеется, это намного увлекательнее, чем оптимизация скорости, да и на скорость редактирования не слишком влияет. Ровно поэтому я и хочу поэкспериментировать с этим.
Собственно файл SVG

Давайте начнем с симпатичного и несложного рисунка с небольшим количеством узлов. Контуры объединены, там где это возможно, по возможности используются клоны, а определения вычищены (в Inkscape: «Файл→Очистить defs»). Все это делается прямо в Inkscape.
viewBox

Поскольку этот элемент нужен для просмотра рисунка в браузере, для начала нужно добавить атрибута viewBox и удалить элементы ширины и высоты (как в постинге A New Can of Worms - SVG as Website Graphics).

Например:

<svg [...] width="640" height="480">

Становится:

<svg [...] viewBox="0 0 640 480">
Шаг 1: Делаем контуры более компактными

На первом шаге используем программу PathC.java, который создаст более компактное описание контуров, чем это делает Inkscape. Кроме того, оно может понизить точность. Для этого изображения и типичных разрешений рабочего стола одного разряда десятичной дроби вполне достаточно. При нуле десятичных дробей изменения уже достаточно радикальны, при двух изображение идеально, а при одной изменения достаточно несущественны. Сценарий также удаляет контуры с пустым атрибутом d. Хотя пустой атрибут d совершенно валиден, он также отключает визуализацию элемента.

Имейте в виду: разбор контуров достаточно примитивен — сценарий даже не может справиться с создаваемыми им самим данными контуров (хотя успешно справляется с контурами Inkscape).import java.io.*;
import java.text.*;
import java.util.*;
import javax.xml.parsers.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import org.xml.sax.*;

public class PathC{
static Document document;
static int osum=0;
static int nsum=0;
static int esum=0;
static DecimalFormat nf;
static int decimalPlaces=1;

public static void main(String[]args){
if(args.length!=2&&args.length!=3){
System.err.println("Usage: java PathC infile outfile <decimal places (default=1)>");
System.exit(1);
}
if(args.length==3){
decimalPlaces=Integer.parseInt(args[2]);
}
String formatPattern="#";
if(decimalPlaces>0){
formatPattern+=".";
for(int i=0;i<decimalPlaces;i++)
formatPattern+="#";
}
nf=new DecimalFormat(formatPattern);

DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
try{
DocumentBuilder builder=factory.newDocumentBuilder();
document=builder.parse(new File(args[0]));

NodeList paths=document.getElementsByTagName("path");
System.out.println(paths.getLength()+" path elements found");
for(int i=paths.getLength()-1;i>=0;--i){
Node path=paths.item(i);
if(path.hasAttributes()){
NamedNodeMap map=path.getAttributes();
for(int k=map.getLength()-1;k>=0;--k){
Node att=map.item(k);
String name=att.getNodeName();
if(name.equals("d")){
String val=att.getNodeValue();
if(val.trim().length()==0){
Node parent=path.getParentNode();
parent.removeChild(path);
esum++;
break;
}
att.setNodeValue(compact(val));
}
}
}
}
System.out.println("path data in : "+osum+" bytes");
System.out.println("path data out : "+nsum+" bytes");
System.out.println("path data saved: "+(osum-nsum)+" bytes");
System.out.println("path data ratio: "+String.format("%.2f",(100.0/(double)osum*(double)nsum))+"%");
if(esum>0)
System.out.println("removed "+esum+" paths with empty path data");

TransformerFactory tFactory=TransformerFactory.newInstance();
Transformer transformer=tFactory.newTransformer();

DOMSource source=new DOMSource(document);
StreamResult result=new StreamResult(new FileOutputStream(args[1]));
transformer.transform(source, result);

}catch(Exception e){
e.printStackTrace();
}
}
private static String compact(String s){
osum+=s.length();
char mode=' ';
char lastMode=mode;
//"M 100 200 L 200 100 L -100 -200"
//"M 100 200 L 200 100 -100 -200"
//"M 100 100 L 200 200"
//"M100 100L200 200"
//mzlhvcsqta
StringBuilder sb=new StringBuilder(512);
String[] result = s.split("s|,|p{Cntrl}");
boolean lastAddNum=false;
for(int x=0;x<result.length;x++){
String s2=result[x];
boolean isMode=false;
if(s2.length()==1){
char c=s2.charAt(0);
if(c>='a'&&c<='z'||c>='A'&&c<='Z'){
isMode=true;
mode=c;
if(lastMode!=c){
lastMode=mode;
sb.append(mode);
lastAddNum=false;
}
}
}
if(!isMode){
if(s2.length()!=0){
if(lastAddNum)
sb.append(' ');
double d=Double.parseDouble(s2);
sb.append(nf.format(d));
lastAddNum=true;
}
}
}
String ret=sb.toString();
nsum+=ret.length();
return ret;
}
}

Шаг 2: Делаем более компактными стили

Следующей целью являются стили. Программа StyleC.java удаляет все стили, используемые по умолчанию, все стили "fill-", если заливки нет, все стили "stroke-", если обводки нет, и если атрибут стиля в конечном счете пуст, он тоже удаляется.

Имейте в виду: программа мало оттестирована и толком не проверена. Кроме того, она работает только с внутритекстовыми стилями.import java.io.*;
import java.util.*;
public class StyleC{
static String []kill={
"opacity:1",
"color:#000000",
"fill-opacity:1",
"fill-rule:evenodd",
"stroke-width:1px",
"stroke-width:1",
"stroke-linecap:butt",
"stroke-linejoin:miter",
"marker:none",
"marker-start:none",
"marker-mid:none",
"marker-end:none",
"stroke-miterlimit:4",
"stroke-dasharray:none",
"stroke-dashoffset:0",
"stroke-opacity:1",
"visibility:visible",
"display:inline",
"overflow:visible",
"enable-background:accumulate"
};
public static void main(String[]args){
if(args.length!=2){
System.err.println("Usage: java StyleC infile outfile");
System.exit(1);
}
try{
BufferedReader in=new BufferedReader(new FileReader(args[0]));
BufferedWriter out=new BufferedWriter(new FileWriter(args[1]));
String line="";
while((line=in.readLine())!=null){
int start=line.indexOf("style="");
if(start>=0){
start+=7;
int end=line.indexOf('"', start);
StringBuilder sb=new StringBuilder(64);
String[]styles=line.substring(start, end).split(";");
boolean killStroke=false;
boolean killFill=false;
for(int i=0;i<styles.length;i++){
if(styles[i].equals("stroke:none")){
killStroke=true;
}else if(styles[i].equals("fill:none")){
killFill=true;
}
}
for(int i=0;i<styles.length;i++){
//System.out.println("["+i+"]"+styles[i]);
boolean found=false;
for(int k=0;k<kill.length;k++){
if(styles[i].equals(kill[k])){
found=true;
break;
}
else if(killStroke&&styles[i].startsWith("stroke-")){
found=true;
break;
}
else if(killFill&&styles[i].startsWith("fill-")){
found=true;
break;
}
}
if(!found){
if(sb.length()>0)
sb.append(';');
sb.append(styles[i]);
}
}
if(sb.length()>0){
out.write(line.substring(0, start));
out.write(sb.toString());
out.write(line.substring(end));
}else{//kill style
out.write(line.substring(0, start-8));
out.write(line.substring(end+1));
}
out.newLine();
}else{
out.write(line);
out.newLine();
}
}
out.flush();
out.close();
}catch(Exception e){
e.printStackTrace();
}
}
}

Шаг 3: Удаление нефункциональных групп и прочего мусора

Этот шаг пока не автоматизирован. Я просто прошелся по файлу SVG в текстовом редакторе и убрал все группы, от которых не было проку, оставив одну группу, в которой был обтравочный контур и трансформация. Я также удалил пространство имен Inkscape, которое оставалось из-за регрессии.
Шаг 4: Удаление отступов и новых строк

Опять же, этот шаг не автоматизирован. Просто уберите все отступы, регулярным выражением найдите и замените символы новых строк — вот и все.
Шаг 5: Сжатие при помощи 7-Zip

Поскольку 7-Zip не поддерживает беззаголовочный режим gzip, я переименовал файл в одиночный символ, поскольку имя файла хранится несжатым. Наконец, я сжал его при помощи gzip с экстремальными параметрами.Установка Значение
Уровень сжатия: Ultra
Метод сжатия: Deflate (фиксированное значение)
Размер словаря: 32 Кбайт (фиксированное значение)
Размер слова: 258

Приложение Размер
Inkscape 7, 684 байт
7-Zip 6, 903 байт
Сэкономлено 781 байт (10.16%)

Как видите, просто повторное сжатие с 7-Zip уже экономит примерно 10%, что порядком впечатляет, учитывая то, как мало времени на это затрачено.

Результаты:Шаг На входе На выходе Сэкономлено
1: Сжатие размера 23, 334 байт 17, 868 байт 23.43%
2: Сжатие стилей 17, 868 байт 12, 661 байт 29.14%
3: Удаление нефункциональных групп 12, 661 байт 12, 190 байт 3.72%
4: Удаление отступов и новых строк 12, 190 байт 11, 848 байт 2.81%
5: Сжатие при помощи 7-Zip 11, 848 байт 4, 000 байт 66.24%

Оригинал Оптимизированная
версия

amg6p7.svgz amg6p_p_s_c_i.svgz
7, 684 байт 4, 000 байт
(7zip+viewBox=6, 903 байт) (52.06% от исходного размера файла)

Откройте оба файла в новой вкладке, попереключайтесь с одной на другую и обратно и попробуйте заметить разницу. Она едва заметна, а с дополнительным разрядом ее было бы совсем незаметно, но по-моему оно того не стоит.

Размер уменьшен почти наполовину и это еще далеко не предел. В файле хватает неиспользуемых id, а у тех id, которые используются, достаточно длинные названия. Создание кратчайших возможных имен и использование классов для стилей (опять же, с наикратчайшими названиями) легко убрали бы еще 500 байт.
Примечание
Шаг 4 можно в известном смысле автоматизировать, открыв файл с настройками Inkscape под названием preferences.xml и поменяв значение inlineattrs с 0 на 1. В этом случае разметка
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="[...]"
d="[...]"
id="rect2204"
sodipodi:nodetypes="ccccc" />
</g>

приобретет более компактный вид:
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
<path style="[...]" d="[...]" id="rect2204" sodipodi:nodetypes="ccccc" />
</g>



Источник: http://linuxgraphics.ru