С введением HTML5, в браузерах появился новый очень мощный тег canvas, дающий возможность манипулировать изображениями прямо из Джаваскрипта. Кроме прочего, в нем предусмотрена функция getImageData, дающая доступ непосредственно к массиву пикселей, составляющих картинку. Поскольку пиксели это просто байты, то фактически у нас есть новый механизм пересылки данных с сервера: достаточно запаковать данные внутрь картинки, загрузить картинку через тег img, потом скопировать ее в canvas и прочитать пикселы. Дальше у нас есть просто буфер из байтов, который можно сконвертировать в текст, HTML, Javascript, во что угодно. Особенно полезно тут то, что если изпользовать формат со сжатием, вроде png или gif, то браузер нам его распакует автоматически (jpeg нам не подходит, поскольку там теряется инфрмация). Хотя форматы эти и рассчитаны на графику, алгоритмы сжатия в них достаточно универсальны. Gif, например, использует вариант алгоритма LZW (тот же, что и в юниксовской утилите compress), а Png - вариант алгоритма Deflate (используемый в zip-файлах). Фактически, у нас есть бесплатная встроенная функуция gzdeflate() ! Cначала о распаковке. Эксперименты показывают, что эффективнее всего работают 8-битные форматы с цветовой палитрой: png-8 или gif (Png-8 обычно лучше). Напомню, что в этом случае картинки хранятся в виде палитры - массива из 256 значений цветов (каждый цвет составлен из трех цветовых компонент R,G, и B, то есть красного, зеленого и синего ) - плюс прямоугольный массив пикселей, представленных в виде индексов в палитре. Поскольку getImageData возвращает нам не индексы цветов в палитре, а сами цвета, то удобнее всего использовать палитру, где индекс 0 соответсвует цвету r=0,g=0,b=0 , индекс 1 соответсвует цвету r=1,g=1,b=1 итп Тогда конвертировать цвет в индекс палитры и обратно вообще элементарно. (впрочем, нестандартные палитры могут пригодиться для простейшего криптования данных) HTML-код: Code: <script> function unpack(img) { // создаем DOM-элемент типа canvas var canvas = document.createElement('canvas'); if (!canvas.getContext) return null; // берем его "контекст", в котором содержится // весь функционал var ctx = canvas.getContext('2d'); if (!ctx.getImageData) return null; // устанавливаем размеры var w = canvas.width = img.offsetWidth; var h = canvas.height = img.offsetHeight; canvas.style.width = w+"px"; canvas.style.height = h+"px"; // копируем картинку на canvas ctx.drawImage(img,0,0); // достаем массив цветов var bytes = ctx.getImageData(0,0,w,h).data; var len = bytes.length; var str = ''; // каждый пиксел представлен четырьмя байтами: // r,g,b,a но нам достаточно лишь одного из них for (var i=0; i < len ; i+=4) { str += String.fromCharCode(bytes[i]); } return str; } // использовать распакованную строку function something(str) { if (str === null) return; // продолжаем работать с str // ... } </script> <img src='compressed_data.png' onload='something(unpack(this))'> Паковать данные можно вручную на Фотошопе: для этого требуется открыть исходный файл как 8-bit greyscale RAW и сохранить его как gif или png-8. Если хочется автоматизировать процесс, то нужна библиотека работы с изображениями, поддерживающая данные форматы. К сожалению, мне не удалось заставить PHP работать без глюков, поэтому пришлось перейти на Python и Python Imaging Library. В качестве веб-сервера я использовал Tornado demo.py: Code: import Image import base64 import sys import os import StringIO import tornado.httpserver import tornado.ioloop import tornado.options import tornado.web from tornado.options import define, options class ImgPacker(object): javascript = """ function unpack(img) { var canvas = document.createElement('canvas'); if (!canvas.getContext) return; var ctx = canvas.getContext('2d'); if (!ctx.getImageData) return; var w = canvas.width = img.offsetWidth; var h = canvas.height = img.offsetHeight; canvas.style.width = w+"px"; canvas.style.height = h+"px"; ctx.drawImage(img,0,0); var bytes = ctx.getImageData(0,0,w,h).data; var len = bytes.length; var str = ''; for (var i=0; i < len ; i+=4) { str += String.fromCharCode(bytes[i]); } return str; } """ def __init__(self): self.cache = dict() def pack(self, name, fmt = 'png'): if fmt != 'png' and fmt != 'gif': raise ValueError, 'Incorrect format supplied in options' if name in self.cache: return self.cache[name] f = open(name, 'rb') data = f.read() f.close() pixels = len(data) if pixels <= 1024: w = pixels h = 1 else: h = (pixels + 1024 - 1)/1024 w = 1024 pixels = h * w fill = (pixels - len(data)) data += ' ' * fill img = Image.new('P', (w, h), 0) palette = [] for i in range(256): palette.extend((i, i, i)) img.putpalette(palette) i = 0 for y in xrange(h): for x in xrange(w): c = ord(data[i]) img.putpixel((x, y), c) i += 1 buf = StringIO.StringIO() img.save(buf,fmt) data = buf.getvalue() buf.close() self.cache[name] = data return data define("port", default=8888, help="run on the given port", type=int) define("data", default="demo.js", help="file to serve as an image", type=str) define("format", default="png", help="image format to use", type=str) packer = ImgPacker() class PackerPage(tornado.web.RequestHandler): def get(self): global packer try: name = self.get_argument('n') name = os.path.basename(name) fmt = self.get_argument('f') if fmt == '': fmt = 'png' elif fmt != 'png' and fmt != 'gif': raise ValueError, 'Incorrect format' self.set_header('Content-Type', 'image/' + fmt) self.write(packer.pack(name, fmt)) except IOError, ValueError: self.set_status(400) class MainHandlerPage(tornado.web.RequestHandler): javascript = """ function addScript(s) { if (!s) return; var el = document.createElement('script'); el.type = "text/javascript"; try { // doesn't work on ie... el.appendChild(document.createTextNode(s)); } catch(e) { el.text = s; } document.head.appendChild(el); } """ html = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>ToJs</title> <script>{unpacker_js}</script> <script>{my_js}</script> </head> <body> {loader_html} <h1>some text</h1> some more text </body> </html> """ def loaderInlineHtml(self, name, fmt = 'png'): global packer data = packer.pack(name, fmt) html = "<img src='data:image/{fmt};base64,{data}' style='{style}' onload='{onload}'>".format( fmt = fmt, data = base64.b64encode(data), style = 'visibility:hidden;position:absolute;', onload ='addScript(unpack(this))' ) return html def loaderHtml(self, name, fmt = 'png'): html = "<img src='{packer_url}?n={name}&f={fmt}' style='{style}' onload='{onload}'>".format( packer_url = '/packer', name = name, fmt = fmt, style = 'visibility:hidden;position:absolute;', onload ='addScript(unpack(this))' ) return html def get(self): self.set_header('Content-Type', 'text/html'); d = MainHandlerPage.html.format( unpacker_js = ImgPacker.javascript, my_js = MainHandlerPage.javascript, loader_html = self.loaderHtml(options.data, options.format) ) self.write(d) def main(): tornado.options.parse_command_line() application = tornado.web.Application([ (r"/", MainHandlerPage), (r"/packer", PackerPage), ]) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start() if __name__ == "__main__": main() demo.js: Code: alert('Unpacked successfully') Пример использования проги: Code: python demo.py --port=8888 --data=demo.js --format=png Идем на localhost:8888 и видим алерт из demo.js Пример паковки: jquery-1.9.1.min.png - исходный размер: 92629 байт, размер в формате png: 33839 байт. Сжатие почти в 3 раза Замечание: Картинка с данными должна находиться на том же домене, что и загружающая ее страница, инача canvas выдаст ошибку доступа
Собственно распаковщик занимает всего 304 байта в минимизированном виде. По сравнению с большинством JS-овских библиотек совсем даже маленькая куча: Code: window.unpack=function(b){var a=document.createElement("canvas");if(a.getContext){var c=a.getContext("2d");if(c.getImageData){var d=a.width=b.offsetWidth,a=a.height=b.offsetHeight;c.drawImage(b,0,0);b=c.getImageData(0,0,d,a).data;c=b.length;d="";for(a=0;a<c;a+=4)d+=String.fromCharCode(b[a]);return d}}}; Впрочем, меня подобные методы интересуют скорее с точки зрения обфускации и скрытой передачи информации: хранить, например, криптованные данные в младших битах в картинки итд. Это отдельная большая тема.