Джаваскрипт внутри картинок

Discussion in 'PHP' started by scanislav, 11 Mar 2013.

  1. scanislav

    scanislav Elder - Старейшина

    Joined:
    25 Jun 2010
    Messages:
    87
    Likes Received:
    22
    Reputations:
    31
    С введением 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 раза :cool:

    Замечание: Картинка с данными должна находиться на том же домене, что и загружающая ее страница, инача canvas выдаст ошибку доступа
     
    1 person likes this.
  2. blesse

    blesse Member

    Joined:
    18 Jan 2012
    Messages:
    175
    Likes Received:
    8
    Reputations:
    1
    Правильно я понял, что перед тем как подключить картинку <img src ...> надо кучу кода еще вставить?
     
  3. scanislav

    scanislav Elder - Старейшина

    Joined:
    25 Jun 2010
    Messages:
    87
    Likes Received:
    22
    Reputations:
    31
    Собственно распаковщик занимает всего 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}}};
    Впрочем, меня подобные методы интересуют скорее с точки зрения обфускации и скрытой передачи информации: хранить, например, криптованные данные в младших битах в картинки итд. Это отдельная большая тема.
     
  4. ol1ver

    ol1ver Active Member

    Joined:
    22 Jul 2011
    Messages:
    237
    Likes Received:
    155
    Reputations:
    0
    Баян же