В погоне за оптимизацией сайтов захотел уменьшить количество запросов, не в ущерб размерам оптимизированных файлов.
Цель — передавать в одном файле изображения разных форматов, с разными настройками оптимизации.
Как средство, выбрал data uri и gzip'нутый css файл. Однако IE с data uri работают из рук вон плохо. Но в них есть mhtml. Существовавшая реализация не отвечала моим требованиям, т.к. приходилось 1 файл передавать два раза — раз для IE, в mhtml, и второй для всех остальных, в data uri. В поисках решения наткнулся на статью bolk'а, где описывалось решение для формата jpeg и некоторые теоретические выкладки для gif и png. После почти трехнедельного р��скуривания манов мне удалось реализовать решение для gif и png и автоматизировать процесс для всех трех форматов.
Поскольку изображения передаются в base64, стоит осветить некоторые, связанные с этой кодировкой, моменты
Информация к размышлению:
Base64 в англоязычной викепедии
Формат секций в JPEG:[заголовок][data]
C jpeg все просто и описано у bolk'а:
Смысл в том, что бы строка в CSS выглядела как:

И дешифровалась в IE как:

А другие ее видели как:

Из-за особенностей base64 надо дополнительно передавать некоторое кол-во символов, что бы строка шифровалась\дешифровалась верно. Они вставляются до и после CSS. Количество просчитывалось и подбиралось опытным путем:
/9j/4AA0;background-image:url(data:image/jpeg;base64;00,
Мой скрипт, который это процесс автоматизирует:
Информация к размышлению:
JPEG в англоязычной викепедии
С этим форматом дела обстоят не так хорошо.
Что можно сделать:
Я выбрал второй вариант, это делает base64 строку более читаемой и позволяет конвертировать любые не анимированные gif.
Более менее стандартный вариант секций GIF: [заголовок][размер][data][00]
Многие GIF имеют не совсем корректный порядок полей. Например если сделать `convert jpeg gif` то полученный файл адекватно обрабатываться скриптом не будет. Юзайте GIMP.
Смысл в том, что бы строка в CSS выглядела как:

При том что до всех правок файл выглядел как:

Мой скрипт для автоматизации процесса:
Для анимированного гифа скрипта нет. Я считаю что лучше использовать анимированными CSS sprites.
Теоретические выкладки:
Информация к размышлению:
GIF color tables
Gif specification
После gif это тихая гавань. У секций размер не ограничен, у них 4байтные заголовки и их очень удобно искать. Для сравнения, для gif я ломал голову и дебажил скрипт почти весь день, а для png все сделал за час.
Формат секций в PNG: [размер(4 байта)][data][CRC(4 байта)]
И тут не обошлось без подводных камней. CRC очень важен для IE, если CRC битый то IE не будет отображать картинку. Всем же остальны�� глубоко параллельно битый он или нет.
Многие PNG имеют не совсем верную структуру, во всяком случае, мой скрипт с нми работать не будет, пока не прогнать их через optipng. Помимо оптимизации изображения, эта прога выставит поля в нужном порядке. Также, мною замечено что Photoshop иногда режет поля sRGB и им сохраненные png обрабатываются не всегда.
CSS будем прятать в секции tEXt
PNG надо сразу заоптимизировать с помощью optipng, потом нарезать таким образом, что бы tExt был сразу за IHDR.
В секции tEXt обязательно должен передаваться keyword00, его длина учитывается в общей длине секции. У меня это 'Comment '
Общий порядок:
Было:

Стало:

Скрипт хорошо комментирован, и в спецификации тоже можно многое почерпнуть
IE6 не видит прозрачности, иногда это можно исправить с помощью bKGD выставляя нужный Background color.
После чего запускаем `optipng -fix FILE` что бы исправить CRC секции tEXt
Мой скрипт для автоматизации процесса:
Информация к размышлению:
PNG Basics
PNG Specification
Если используется MHTML то CSS должен быть весь отредактирован под него и разбит на секции(пример в архиве):
Архив с исходниками и скриптами
Пример рабочего сайта
Тестировалось в FF 3.6, Opera 10.10, chromium, chrome, IE6-8
P.S: Автором этой статьи является мой хорший друг Banderlog. Статью размещаю по его просьбе, соответственно вопросы рекомендую задавать ему напрямую в jabber: banderlog@jabber.com.ua
P.P.S: Странно что только на второй день обнаружился тот факт что при посте статьи была допущена ацкая ошибка в скриптах. Все 3 были одинаковыми.
Цель — передавать в одном файле изображения разных форматов, с разными настройками оптимизации.
Как средство, выбрал data uri и gzip'нутый css файл. Однако IE с data uri работают из рук вон плохо. Но в них есть mhtml. Существовавшая реализация не отвечала моим требованиям, т.к. приходилось 1 файл передавать два раза — раз для IE, в mhtml, и второй для всех остальных, в data uri. В поисках решения наткнулся на статью bolk'а, где описывалось решение для формата jpeg и некоторые теоретические выкладки для gif и png. После почти трехнедельного р��скуривания манов мне удалось реализовать решение для gif и png и автоматизировать процесс для всех трех форматов.
BASE64
Поскольку изображения передаются в base64, стоит осветить некоторые, связанные с этой кодировкой, моменты
- Во первых: base64 понимает только [A-Za-z0-9+/].
При дешифровке браузерами или ruby либой все прочее символы дропаются.
Консольный base64 на линуксе их не дропает и выводит ошибку. - Во вторых: base64 конвертит каждые 3 исходных байта (в нашем случае, ASCII символы) в 4 ASCII символа.
Поэтому декодированную CSS строку надо уравновешивать, к примеру, нулями, что бы она правильно себя вела внутри закодированного файла.
Информация к размышлению:
Base64 в англоязычной викепедии
JPEG
Формат секций в JPEG:[заголовок][data]
C jpeg все просто и описано у bolk'а:
Открываем HEX-редактор:
FF D8 — заголовок JPEG для IE
FF E0 — объявление секции APP0, куда прячется всё до данных изображения,
«;background-color:url(data:image/jpeg;base64,» — это видят остальные браузеры.
Когда IE декодирует эту строку, то получается хлам, который ни на что не влияет
FF D8 — начало JPEG для остальных браузеров
«данные изображения» — это место видят уже все браузеры
Смысл в том, что бы строка в CSS выглядела как:

И дешифровалась в IE как:

А другие ее видели как:

Из-за особенностей base64 надо дополнительно передавать некоторое кол-во символов, что бы строка шифровалась\дешифровалась верно. Они вставляются до и после CSS. Количество просчитывалось и подбиралось опытным путем:
/9j/4AA0;background-image:url(data:image/jpeg;base64;00,
Мой скрипт, который это процесс автоматизирует:
#!/usr/bin/ruby
require 'base64'
# тут строка ВСЕГДА равна одному значению:
a="/9j/4AA0;background-image:url(data:image/jpeg;base64;00,"
#Основной файл
b=Base64.encode64(File.open("#{ARGV[0]}",'r'){|f| f.read})
# Запись в файл
File.open('temp','w'){|i| i.write("#{Base64.decode64(a)}#{Base64.decode64(b)}")}
# перегонка файла обратно в base64
#cat test | base64 | tr -d "\n" > jpeg64.txt
File.open('temp2','w'){|o| o.write(Base64.encode64(File.open('temp','r'){|f| f.read}))}
#File.delete('temp')
c=File.open('temp2','r'){|f| f.read}.gsub(/\/9j\/4AA0backgroundimageurldataimage\/jpegbase6400/,"/9j/4AA0;background-image:url(data:image/jpeg;base64;00,").gsub(/\n/,"")
File.open('out_jpeg64','w'){|s| s.write("#{c}\);")}
File.delete('temp2')
# можно вставлять в css
# cat output64 | tr -d "\n"
# и хорошо поверить mhtml!!!
* This source code was highlighted with Source Code Highlighter.Информация к размышлению:
JPEG в англоязычной викепедии
GIF
С этим форматом дела обстоят не так хорошо.
- Во первых, размер его секции задает 1 байт, т.е. максимальная длина секции ff или 255 символов.
- Во вторых: размер секции Comments почему-то ограничен размером в 240 байт, и 30 символов занимает CSS строка и еще несколько надо для 'уравновешивания' base64.
- В третьих: Есть всего 2 блока, куда можно запихнуть 'мусор' — Application Extension и Comment Extension и они не могут идти перед General Color table. А таблица цветов может занимать максимум 256*3=768 байт.
Что можно сделать:
- Не трогать General Color Table, если кол-во цветов не превышает ~70
- Переместить содержимое General Color Table в Local Color Table
Я выбрал второй вариант, это делает base64 строку более читаемой и позволяет конвертировать любые не анимированные gif.
Более менее стандартный вариант секций GIF: [заголовок][размер][data][00]
Многие GIF имеют не совсем корректный порядок полей. Например если сделать `convert jpeg gif` то полученный файл адекватно обрабатываться скриптом не будет. Юзайте GIMP.
Первые 13 байт это та инфа, сокращать которую нельзя. Причем 11 байт является сложно-составным и описывает Global Color Table. Его меняем на 00
Вырезаем таблицу цветов (от 14 байта и до камента — 21 FE xx, где xx — размер комм��нта)
Коммент с css и первыми 13ю байтами.
Вырезаем таблицу цветов (от 14 байта и до камента — 21 FE xx, где xx — размер коммента)
'Внутренний кoммент' длиной в 1 символ
Вырезаем таблицу цветов (от 14 байта и до камента — 21 FE xx, где xx — размер коммента)
2c 00 00 00 00 — Image descriptor. Его 10й байт является сложно-составным и описывает Local Color Table. Переносим из 11-го байта все что переносится (объявить Local Color Table, сортирована\нет, размер Local color table), подробнее в спецификации формата.
Вставляем таблицу цветов
Продолжение Image descriptor
Смысл в том, что бы строка в CSS выглядела как:

При том что до всех правок файл выглядел как:

Мой скрипт для автоматизации процесса:
#!/usr/bin/ruby
# CONVERT INCORRECTLY TRANSFER DATA. USE GIMP INSTEAD
# USE: ./GIF_SCRIPT.RB [GIF_FILE]
require 'base64'
# OPEN GIF FILE IN HEX
orig=File.open("#{ARGV[0]}",'r'){|f| f.read.unpack("H*")}.to_s
# FUTURE HEADER
header=orig[0..25]
# GREP GENERAL COLOR TABLE
# [26..1565]/6 = 256 BYTE (MAX SIZE OF COLOR TABLE)
color_table=orig[26..1565][/(.*)21fe/,1]
if color_table.class == NilClass
color_table=orig[26..1575][/(.*?)2c0000/,1]
end
# FOR DEBUGING
#puts color_table
#puts color_table.length
puts "COLORS IN PALLETE: #{color_table.length/6}"
# GIF IMAGE DATA
data=orig[/2c0000.*/]
# SAVE 11 BYTE'S INFO AND ADOPT IT FOR LOCAL COLOR TABLE
eleven=header[20..21].to_i(16).to_s(2)
local_mix="10#{eleven.split("")[4].to_s}00#{eleven.split("")[5..7].to_s}".to_i(2).to_s(16)
# 11 BYTE TO ZERO
header[20..21]="00"
# DECLARE LOCAL COLOR TABLE
data[18..19]=local_mix
# MAGIC COMMENT
comment=Base64.decode64(";background-image:url(data:image/gif;base64;pzd,").unpack("H*").to_s
# WRITE ALL IN ONE FILE
var=header+"21fe313030"+comment+header+"21fe013000"+data[0..19]+color_table+data[20..-1]
File.open('out.gif','w'){|f| f.write(var.to_a.pack("H*"))}
# ENCODE FILE TO BASE64 WITH "\n" REMOVING
File.open('temp','w'){|o| o.write(Base64.encode64(File.open('out.gif','r'){|f| f.read}).gsub(/\n/,""))}
# MAKE STRING CSS READEABLE
c=File.open('temp','r'){|f| f.read}.gsub(/backgroundimageurldataimage\/gifbase64pzd/,";background-image:url(data:image/gif;base64;pzd,").gsub(/\n/,"")
File.delete('temp')
# JUST PASTE TEXT FROM THIS FILE TO CSS
File.open('out_gif64','w'){|s| s.write("#{c}\);")}
* This source code was highlighted with Source Code Highlighter.Для анимированного гифа скрипта нет. Я считаю что лучше использовать анимированными CSS sprites.
Теоретические выкладки:
- Для каждого кадра делать Local Color Table смысла не имеет, т.к. это увеличит размер.
- Анимированные гифы с кол-вом цветов 64 могут обрабатываться с включением General Color Table в коммент
- Application Extension и Comment Extension могут идти подряд, что увеличивает возможный размер x2.
- На просторах интернета я встретил информацию о том, что в Application Extension фактически 2 блока задают размер.
21 ff SizeSize 'NETSCAPE2.0' SizeSize 01 00 00, где SizeSize — 2 байта отвечающие за размер, а 01 байт отвечающий за infinitive loop.
- Что, в теории, может предоставить возможность 'забить' большее кол-во цветов. Но все-равно меньше 256 (около 230).
Информация к размышлению:
GIF color tables
Gif specification
PNG
После gif это тихая гавань. У секций размер не ограничен, у них 4байтные заголовки и их очень удобно искать. Для сравнения, для gif я ломал голову и дебажил скрипт почти весь день, а для png все сделал за час.
Формат секций в PNG: [размер(4 байта)][data][CRC(4 байта)]
И тут не обошлось без подводных камней. CRC очень важен для IE, если CRC битый то IE не будет отображать картинку. Всем же остальны�� глубоко параллельно битый он или нет.
Многие PNG имеют не совсем верную структуру, во всяком случае, мой скрипт с нми работать не будет, пока не прогнать их через optipng. Помимо оптимизации изображения, эта прога выставит поля в нужном порядке. Также, мною замечено что Photoshop иногда режет поля sRGB и им сохраненные png обрабатываются не всегда.
CSS будем прятать в секции tEXt
PNG надо сразу заоптимизировать с помощью optipng, потом нарезать таким образом, что бы tExt был сразу за IHDR.
В секции tEXt обязательно должен передаваться keyword00, его длина учитывается в общей длине секции. У меня это 'Comment '
Общий порядок:
IHDR
tExt
Другая служебная информация
data
Было:

Стало:

Скрипт хорошо комментирован, и в спецификации тоже можно многое почерпнуть
IE6 не видит прозрачности, иногда это можно исправить с помощью bKGD выставляя нужный Background color.
После чего запускаем `optipng -fix FILE` что бы исправить CRC секции tEXt
Мой скрипт для автоматизации процесса:
#!/usr/bin/ruby
#
#!!!! RUN optipng FIRST !!!!
#
# USE: ./PNG_SCRIPT.RB [PNG_FILE]
require 'base64'
# OPEN GIF FILE IN HEX
orig=File.open("#{ARGV[0]}",'r'){|f| f.read.unpack("H*")}.to_s
#ihdr=orig[0..65]
ihdr=orig[/(.*?)73524742/,1][0..-9]
#sRGB - 73 52 47 42 & -4b (8 characters)
#srgb_phys=orig[66..171]
#check for tEXt existence
if orig[/74455874/].class == NilClass
srgb_phys=orig[/(.{8}73524742.*?)49444154/,1][0..-9]
else
srgb_phys=orig[/(.{8}73524742.*?)74455874/,1][0..-9]
end
#srgb_phys=orig[/(.{8}73524742.*?)74455874/,1][0..-9]
#tEXt - 74 45 58 74 –њ–Њ—Б–ї–µ–і–љ–Є–µ 8 –љ–∞–і–Њ –Љ–µ–љ—П—В—М –љ–∞ CRC 00000000
#text=orig[172..245]
#text=orig[/(.{8}74455874.*?)49444154/,1][0..-9]
#IDAT - 49444154
#data=orig[246..-1]
data=orig[/.{8}49444154.*/]
#MAGIC COMMENT
comment=Base64.decode64(";background-image:url(data:image/png;base64;pzd,").unpack("H*").to_s
###### OUTER PNG
# "00000059"+"74455874"+"436f6d6d656e7400"
# tEXt_length + 'tEXt' + 'Comment.'
# "3030" - two zero for base64 balance
###### INNER PNG
# "00000008"+"74455874"+"436f6d6d656e7400"+"00000000"
# min_tEXt_length + 'tEXt' + 'Comment.' + blank CRC
#
# CRC field one for two PNG's
# IE can't live without it, but others feel indifferently
var=ihdr+"00000059"+"74455874"+"436f6d6d656e7400"+"3030"+comment+ihdr+"00000008"+"74455874"+"436f6d6d656e7400"+"00000000"+srgb_phys+data
File.open('out.png','w'){|f| f.write(var.to_a.pack("H*"))}
# CRC FIX
puts "optipng -fix started..."
`optipng -fix out.png`
puts "optipng -fix completed"
# ENCODE FILE TO BASE64 WITH "\n" REMOVING
File.open('temp','w'){|o| o.write(Base64.encode64(File.open('out.png','r'){|f| f.read}).gsub(/\n/,""))}
# MAKE STRING CSS READEABLE
c=File.open('temp','r'){|f| f.read}.gsub(/backgroundimageurldataimage\/pngbase64pzd/,";background-image:url(data:image/png;base64;pzd,").gsub(/\n/,"")
File.delete('temp')
# JUST PASTE TEXT FROM THIS FILE TO CSS
File.open('out_png64','w'){|s| s.write("#{c}\);")}
* This source code was highlighted with Source Code Highlighter.Информация к размышлению:
PNG Basics
PNG Specification
MHTML
Если используется MHTML то CSS должен быть весь отредактирован под него и разбит на секции(пример в архиве):
/*
Content-Type: multipart/related; boundary="_"
--_
Content-Type: text/css;
*/
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#half_logo {
/*
--_
Content-Location:logo
Content-Transfer-Encoding:base64
Content-Type: image/png;*/
iVBORw0KGgoAAAANSUhEUgAAAT4AAAA3CAMAAACintZ+AAAAWXRFWHRDb21tZW50ADAw;background-image:url(data:image/png;base64;pzd,iVBORw0K...);
/*
--_
Content-Type: text/css;
*/
background-image: url(mhtml:http://192.168.1.2/test.css!logo) !ie;
/*
--_--
*/
* This source code was highlighted with Source Code Highlighter.Архив с исходниками и скриптами
Пример рабочего сайта
Тестировалось в FF 3.6, Opera 10.10, chromium, chrome, IE6-8
P.S: Автором этой статьи является мой хорший друг Banderlog. Статью размещаю по его просьбе, соответственно вопросы рекомендую задавать ему напрямую в jabber: banderlog@jabber.com.ua
P.P.S: Странно что только на второй день обнаружился тот факт что при посте статьи была допущена ацкая ошибка в скриптах. Все 3 были одинаковыми.