Pull to refresh

К вопросу о кроссбраузерных Data URI

Reading time 8 min
Views 9.8K
В погоне за оптимизацией сайтов захотел уменьшить количество запросов, не в ущерб размерам оптимизированных файлов.
Цель — передавать в одном файле изображения разных форматов, с разными настройками оптимизации.
Как средство, выбрал 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 были одинаковыми.
Tags:
Hubs:
+63
Comments 87
Comments Comments 87

Articles