пятница, 18 декабря 2015 г.

Конвертирование сферической панорамы в кубическую

Преамбула:

Как известно Adobe Flash Player, скажем так, подвергается гонениям со стороны браузеров всех мастей.

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

Если простые анимашки перевести на рельсы HTML5 достаточно просто, то с панорамой немного посложнее.

Выбор вьювера для панорамы пал на Pannellum.

Основные преимущества:

  • Опенсёрсный;
  • Шустрый;
  • Имеет кроме просмотра доп. возможности по размещению меток (метки отображаются в DOM, а не на canvas);
  • Имеет богатый набор опций для конфигурирования.



Проблема:

Pannellum отображает кубические панорамы, а у клиента сферическая. Встала проблема конвертации панорамы из сферической в кубическую.

В ходе поиска попадались как платные так и бесплатные утилиты. Но одни требовали "много денег", другие установку доп. софта. И вот чудом-юдом попалась одна подходящая опенсёрсная, без регистрации и смс установки стороннего софта и что тоже не маловажно кроссплатформенная.



Решение:
  1. Проверяем есть ли на компе java. Если нет, то качаем/устанавливаем (достаточно JRE).
  2. Качаем/распаковываем архив с утилитой от сюда или от сюда.
    Из всего архива нужен по сути только файл EquirectangularToCubic.jar
  3. Запускаем утилиту:

    java -jar EquirectangularToCubic.jar path_to_image.jpg
    

    Для примера можно взять эту сферическую панораму.


    На выходе получаем папку:
    cubic_path_to_image
        |__examplepano_b.jpg
        |__examplepano_d.jpg
        |__examplepano_f.jpg
        |__examplepano_l.jpg
        |__examplepano_r.jpg
        |__examplepano_u.jpg
    

Примечания:

  • Дополнительные ключи для запуска утилиты можно подсмотреть в файле EquirectangularToCubic.java в методе parseCommandLine
  • Важно! Для корректного отображения панорамы в Pannellum-е необходимо сделать переименование файлов:
    *_b.jpg ↔ *_f.jpg
    *_u.jpg ↔ *_r.jpg

пятница, 17 июля 2015 г.

Переход на страницу профиля пользователя Facebook по его ид

понедельник, 5 ноября 2012 г.

Антикэш на хэшах ресурсов: Вступление

Преамбула:
  • Есть некое приложение, которое в ходе своей работы использует множество внешних ресурсов/файлов;
  • Приложение работает в среде интернет-браузера;
  • Всего внешних ресурсов/файлов более 3000 единиц;
  • От релиза к релизу часть этих ресурсов может изменятся (например, художник перерисовал изображения некоторых айтемов или фоновых изображений, добавлены новые строковые, нормализованы по уровню громкости фоновые звуки, и прочее).

Итоговый алгоритм определения "протухшего" ресурса в кэше браузера определяется связкой "интернет-браузер + файловый сервер".
Подробнее о кэшировании можно почитать тут или воспользоватся результатами поиска.

Данная тема затронута в докладе на встрече RAFPUG, прошедшей в мае этого года в г.Челябинске (см. презентацию).

Проблема:
Алгоритм определения факта "протухания" файла в кэше браузера напрямую зависит от самого браузера и настроек отдающего файлового сервера.

Для уверенности в том, что клиентское приложение загрузит последнюю версию ресурса, необходимо принудительно сбросить кэш для всех ресурсов.
Сброс кэша осуществлялся добавлением в конец URL ресурса случайного параметра, например:
http://mysite.ru/images/bg.png?<random string>
Зачастую в качестве рандомного параметра используется номер версии приложения.

При выходе нового релиза возникнут следующие проблемы:
  • клиентское приложение заново грузит ресурсы, не взирая на то, что они не "протухли" в кэше браузера и остались неизменными с момента предыдущего релиза;
  • появится лишняя нагрузка на отдающий файловый сервер.

Следовательно, требуется добиться следующих результатов:
  1. Уверенность в том, что клиентское приложение будет использовать последнюю версию ресурса;
  2. Независимость от алгоритма поиска ресурса в кэше браузера;
  3. Снижение нагрузки на отдающий файловый сервер.

Решение:
Способ №1:
  1. Сформировать словарь соответствий относительных URL файлов с их хэшами, например: файлу
    ./images/bg.png
    соответствует хэш
    bb74f737c85c996cc2ec29c63698bf80
  2. Использовать в качестве антикэш-параметра значение хэша из словаря при формировании URL для загрузки файла, например:
    http://mysite.ru/images/bg.png?bb74f737c85c996cc2ec29c63698bf80
    Если соответствия не найдено — использовать строку с номером последней версии приложения.

Способ №2:
Данный способ был подсказан Михаилом Антипиным (Nox Noctis), за что ему огромное чистосердечное спасибо.
  1. Сформировать словарь соответствий оригинальных имен файлов с их новыми именами, например: оригинальному имени файла
    bg.png
    соответствует новое имя
    00240736ec53d1f23366122e8d12db5f9f06f7c70404000304d8c000
    В новое имя файла может быть "зашит" не только его хэш, но и дополнительные данные, например: размер файла в байтах, тип (png, jpg, ...), дату последнего изменения и прочее.
  2. Переименовать файлы в соответствии со словарем и переместить в общую для внешних ресурсов папку, например:
    http://mysite.ru/files/
  3. Определять его новое имя файла при формировании URL, по оригинальному имени используя словарь, например:
    http://mysite.ru/files/00240736ec53d1f23366122e8d12db5f9f06f7c70404000304d8c000

P.S.: Оба решения успешно протеситрованны и используются в боевых условиях.
В дальнейшем в данном посте будут размещены ссылки на описания реализаций каждого способа, а так же рассмотрены преимущества и недостатки каждого из них.

вторник, 30 октября 2012 г.

navigateToURL в цикле (Action Script)

Преамбула:
Возникла необходимость последовательно открывать в браузере несколько ссылок на ресурсы.

Проблема:
При последовательном или в цикле вызове функции navigateToURL отрабатывает всего лишь один вызов.

Решение:
  1. Приложение работает в standalone flashplayer-е или в среде flash-плагина браузера:
    const URLS:Array = ["http://ya.ru", "http://r0.ru"];
    stage.addEventListener(Event.ENTER_FRAME, function (e:Event):void {
        navigateToURL(new URLRequest(URLS.shift()), "_blank");
        if (URLS.length == 0) {
            e.target.removeEventListener(e.type, arguments.callee);
        }
    });
    
    Прим.: В качестве источника события Event.ENTER_FRAME можно использовать любой экземпляр класса Sprite или его наследников в независимости добавлен ли этот экземпляр в список отображения или нет.
  2. Приложение работает в среде flash-плагина браузера, доступен и разрешен вызов javascript-методов, то можно обойтись без подписки на событие Event.ENTER_FRAME:
    const URLS:Array = ["http://ya.ru", "http://r0.ru"];
    if (ExternalInterface.available) {
        for each (var url:String in URLS) {
            ExternalInterface.call("window.open", url);
        }
    }
    

пятница, 26 октября 2012 г.

As3/Flex offline-документация на русском (Action Script, Flex)

Преамбула:
Взбрендило мне заиметь документацию по as3 оффлайн - на тот случай когда интернета нет, а очень надо.

Проблема:
Поверхностный поиск в google не дал приемлемых результатов: ни по русской, ни по английской версии нет удобоваримой документации по классам as3, которую можно качнуть архивом.

Решение:
Нашлась программка Doc, написанная на AIR-е, в которой можно добавить путь до "книги" (в моем случае он http://help.adobe.com/ru_RU/FlashPlatform/reference/actionscript/3/). Затем она очень долго её выкачивала (~800 MB), индексировала и писала все это в локальную SQLite-базу.

К слову сказать, мне эта прога не понравилась: неудобно - внутристраничные ссылки не обрабатываются, т.е. при клике на метод или поле перехода в нужное место на странице не происходит.

НО! Главное, что эта программка скачала требуемую документацию пригодную для offline-использования. Спасибо ей за это. Забираем скачанное, анинсталим программку, пользуемся докой.

На случай ядерной войны выложил плотно упакованный 7-zip-ом архив с документацией:
Версия документации по состоянию на октябрь 2012.
As3DocumentatonOffline.7z (~11 MB, MD5: 891d9c8d6d4ba211fbd9087a2adf8f4e)

среда, 24 октября 2012 г.

Доступ к ресурсам защищенным Basic authentication (ActionScript, Basic Authorization)

Преамбула:
При разработке десктопного приложения на AIR столкнулся с проблемой доступа к ресурсам защищеным Basic access authentication или по-простому базовой авторизацией. Причем ресурсы расположены на разных серверах, условно site1.ru и site2.ru.
  • site1.ru – серверная часть приложения
  • site2.ru – хранилище статических ресурсов (картинки, флешки и прочая мультимедия)
При работе аналогичного приложения в среде flash-плагина браузера проблем нет, прошел авторизацию на каждом сервере один раз за сеанс работы браузера и шпарь себе дальше.

Проблема:
Для базовой авторизации необходимо в HTTP-запросе передавать заголовок Authorization с данными по логину и паролю.
Добавить нужный заголовок в URLRequest не представляется возможным из-за ограничений URLRequestHeader.
Нельзя, а хочется, воспользоватся такими средствами как загрузчик LoaderMax.

Решение:
Способ №1
Есть отличная библиотека as3httpclient с реализацией на сокетах аналога URLLoader-а и URLRequest-а которые позволяют добавить необходимые заголовки в HTTP-запрос.
var loader:SocketURLLoader = new SocketURLLoader();
loader.addEventListener(Event.COMPLETE, onComplete);

var request:SocketHTTPRequest = new SocketHTTPRequest();
request.url = "http://site1.ru";
request.requestHeaders = [new URLRequestHeader("Authorization", "Basic dXNlcjpwYXNzd29yZA==")];
loader.load(request);
dXNlcjpwYXNzd29yZA== 
это сериализованная в base64 строка "user:password".
Этот способ хорош для выполнения запросов обращеных к серверу бизнес-логики приложения и не удобен для загрузки статических ресурсов (в частости изображений) которые придется грузить в бинарном виде, заталкивать их в экземпляр класса Loader или другой класс для использования загруженного ресурса.

Способ №2
Как оказалось все уже придумано самими адобами.
Надо по-внимательней смотреть в документацию!
Встречайте URLRequestDefaults.
Достаточно один раз указать данные для авторизации, скажем в конструкторе главного класса приложения:
URLRequestDefaults.setLoginCredentialsForHost("site1.ru", "user", "password");
URLRequestDefaults.setLoginCredentialsForHost("site2.ru", "user", "password");
URLRequestDefaults.authenticate = true;

вторник, 22 мая 2012 г.

RAFPUG в Челябинске (19.05.2012)

Двинул речь вот тут про это.

Особое спасибо Михаилу Антипину (техническому директору mimimigames.ru ) за помощь по теме антикеша на хешах ресурсов.

P.S.: Спасибо тем, кто прослушал мою "тираду". Объясняльщик из меня еще тот ;)

пятница, 27 апреля 2012 г.

Подстановка параметров в шаблон строки (ActionScript, string template)

Преамбула:
Имеются множество шаблонов строк как для вывода сообщений об ошибках, так и просто каких то фраз в диалоговых окнах.
Шаблон строки представляет из себя следующее: "Мама мыла {0} и гладила {1}, а потом еще разок помыла {0}. Наша мама {2}!".

Проблема:
Заменить параметры в шаблоне строки реальными значениями.

Решение:
Способ №1
/**
 * Подстановка параметров в шаблон строки
 * @param  source - шаблон строки
 * @param  ...params - параметры, если первый параметр массив то параметры берутся от туда
 * @return
 */
public function replaceParams(source:String, ...params):String {
  if (!source || !params || !params.length)
    return source;
  
  if (params[0] is Array)
    params = params[0];
    
  var regexp:RegExp = null; 
  for (var i:int = 0; i < params.length; i++) {
    regexp = new RegExp("\\{" + i + "\\}", "g"); 
    source = source.replace(regexp, params[i]);
  }
  
  return source;
}

// Пример:
var source:String = "Мама мыла {0} и гладила {1}, а потом еще разок помыла {0}. Наша мама {2}!";
trace(replaceParams(source, "раму", "штаны", "молодец"));
// or
trace(replaceParams(source, ["раму", "штаны", "молодец"]));
// выведет: Мама мыла раму и гладила штаны, а потом еще разок помыла раму. Наша мама молодец!
Прим.: как оказалось такого рода функционал по подмене параметрами реализован в методе StringUtil.substitute(), о чем я узнал немного позже. Ну да ладно, свой велос ближе к телу :)

Способ №2 (более монструозный и менее производительный)
/**
 * Вставка массива параметров
 * @param str - шаблон строки
 * @param params - массив с параметрами
 * @return строка заполненная параметрами
 */
public static function insertParametersIntoString(str:String, params:Array):String {
  if (str == "" || str == null) 
    throw new Error("Insert parameter in 'null' string. Target string = " + str + ". Number of parameter = " + params.length + ". Parameters = " + params.toString());
  
  for (var i:int = 0; i < params.length; i++) {
    try {
      str = strInsertParameter(str, i, params[i].toString());
    } catch (err:Error) {
      throw new Error("Insert null parameter in string. Target string = " + str + ". Number of parameter = " + i + ". Parameter = " + params[i].toString() + "Error: " + err);
    }
  }
  
  return str;
}

/**
 * Вставка параметра в указанную позицию
 * @param str - шаблон строки
 * @param paramIndex - индекс параметра в шаблоне
 * @param paramValue - значение параметра
 * @return строка заполненная параметрами
 */
public static function strInsertParameter(str:String, paramIndex:int, paramValue:String):String {
  var resultStr:String;
  var searchStr:String;
  var i:int;
  searchStr = "{" + paramIndex + "}";
  i = str.lastIndexOf(searchStr);
  if (i == -1)
    return str;

  while (i != -1) {
    str = str.slice(0, i) + paramValue + str.slice(i + searchStr.length, str.length);
    i = str.indexOf(searchStr);
  }

  return str;
}

// Пример:
var source:String = "Мама мыла {0} и гладила {1}, а потом еще разок помыла {0}. Наша мама {2}!";
trace(insertParametersIntoString(source, "раму", "штаны", "молодец"));
// выведет: Мама мыла раму и гладила штаны, а потом еще разок помыла раму. Наша мама молодец!

воскресенье, 8 апреля 2012 г.

Имитация ячейки scratch-лотереи (ActionScript, GameDev)



Преамбула:
Понадобилось как то раз в игре сделать такую вещь:
  1. Дан массив ячеек со скрытым содержимым
  2. Пользователь юлозя мышкой по ячейке открывает содержимое
Проблема:
Сделать процесс открытия ячейки похожим на процесс стирания в scratch-лотерее.

Решение:
Проблема сводится к решению двух задач:
  1. Отобразить результат стирания
    • накладываем на скрытый контент маску
    • "рисуем" на маске "стиралкой", тем самым открывая контент
  2. Проверить факт стирания достаточной площади ячейки для её открытия
    • при отпускании кнопки мыши снимаем битмапу со спрайта где отображается результат работы маски
    • смотрим какой процент прозрачных пикселей остался и принимаем решение открыть ячейку или нет

ScratchSlot

Alternative content

Get Adobe Flash player



Исходник (FlashDevelop project, 201 кб)

вторник, 6 марта 2012 г.

Пакетная проверка xml-файлов на валидность (ant)

Преамбула:
  • Приложение подгружает языковой xml-файл (соответствующий переданной локале) со строковыми в стандарте xliff.
  • Архитектура приложения такова, что если загруженный файл не валидный то приложение "прекращает работу".
  • Языковых файлов около 15 штук.
  • Загрузкой файлов на сервер занимается ant-build-скрипт.

Проблема:
Не допустить загрузку не валидных xml-файлов.

Решение:
Воспользуемся консольной утилитой xmllint, которая есть в *nix системах, а так же в комплекте cygwin для windows.
Например, файл для русского языка находится по пути: ${projectDir}\bin\ru\client_ru.xlf
Вот таким таском проверим перед загрузкой нужные нам языковый файлы:

   Validating lang files...
      
         
         
         
         
         ...
   
   OK

P.S.: Можно еще более ужесточить проверку подсунув xmllint дополнительно DTD описывающего структуру xml-файлов.

среда, 21 декабря 2011 г.

LoaderContext, утечки памяти (ActionScript)

Как бороться c утечками памяти при загрузке внешних swf-модулей рассказано в блоге Максима Фирсова:
http://snzflash.com/blog/?p=54

Программное создание transparency grid, check-текстуры/заливки (ActionScript)

Преамбула:
Частенько, для проверки объектов с прозрачностью, нужно было подкладывать что то пёстрое под них для того, чтобы лучше их визуально оценить. Приходилось каждый раз делать каляки-маляки в graphics низлежащего Shape/Sprite-а.
Не соврать, подобные каляки называются "transparency grid" или check-текстурой/заливкой в случае 3D-контекста.

Проблема:
Не заморачиваясь, легко и задорно создавать "transparency grid" в поданном на вход экземпляр Graphics.

Решение:
Не мудрствуя лукаво, решил сделать "transparency grid" как в фотошопе + задать возможность кастомизации параметров этой текстуры (на всякий случай).

Конечный результат приведенного ниже кода:

transparency grid
/**
 * Рисует check-текстуру
 * @param   canvas - экземпляр класса Graphics
 * @param   width - ширина требуемой текстуры
 * @param   height - высота требуемой текстуры
 * @param   cellSize - размер ячейки
 * @param   cellColors - массив с цветами для нечетной и четной ячеек в формате RGBA
 * @param   lineColor - цвет линии между ячейками в формате RGBA
 */
public function drawCheckFill(canvas:Graphics, width:Number, height:Number, cellSize:Number, cellColors:Array = null, lineColor:uint = 0x080808FF):void
{
   var colors:Array = cellColors || [0x111111FF, 0x202020FF];
   colors = colors.slice(0, 2);
   
   var cols:int = int(width / cellSize);
   var rows:int = int(height / cellSize);
   var color:uint = 0;
   for (var i:int = 0; i < cols; i++) 
   {
      for (var j:int = 0; j < rows; j++) 
      {
         color = colors[(i + j) % 2];
         canvas.beginFill(color >> 8, (color & 0x000000FF) / 0xFF);
         canvas.drawRect(i * cellSize, j * cellSize, cellSize, cellSize);
      }
   }
   canvas.endFill();

   canvas.lineStyle(1, lineColor >> 8, (lineColor & 0x000000FF) / 0xFF, true, LineScaleMode.NORMAL, CapsStyle.NONE);
   for (i = 0; i < cols; i++) 
   {
      canvas.moveTo(i * cellSize, 0);
      canvas.lineTo(i * cellSize, rows * cellSize);
   }
   
   for (i = 0; i < rows; i++) 
   {
      canvas.moveTo(0, i * cellSize);
      canvas.lineTo(cols * cellSize, i * cellSize);
   }
}      

var shape:Shape = new Shape();
drawCheckFill(shape.graphics, 170, 170, 8, [0XFFFFFFFF, 0xCCCCCCFF], 0);
P.S.: Кстати для создания текстур для материалов 3D-движков тоже сгодится.

воскресенье, 18 декабря 2011 г.

Пара приемов для windows-консоли (Windows, console)

Прием #1

Преамбула:
Как то давно стояла задача со следующей последовательностью действий:
  1. В БД Oracle запускался по шедулеру таск который брал файл(ы) из определенной директории, парсил их и складывал данные в таблицы, после чего запускался bat-файл который выполнял пункты ниже.
  2. Перенос файлов в zip/rar-архив.
  3. Перенос архивов в бекапную папку.
Так вот, экспериментальным путем было установлено, что надо делать небольшую паузу между п.2 и п.3 (видимо архиватор к моменту запуска п.3 не успевал "отпустить" архив).

Проблема:
Сделать паузу скушать твикс между задачами в bat-файле.

Решение:
Т.к. для паузы в батнике я ничего стандартного не нашел (какойнить wait, delay, sleep), то применил вот такой финт ушами:
ping localhost -n <примерное время в секундах>

Прием #2

Преамбула:
Есть задача со следующей последовательностью действий:
  1. Обновить проект (на тему появления новых строковых и графических ресурсов).
  2. Пройтись обфускатором по уже откомпеленному клиенту. (task secureClient)
  3. Залить клиента и ресурсы на сервер. (task upload)
п.2 и п.3 прописаны в ant-товском build.xml наряду с дугой кучей тасков.
Дефолтный таск = upload.
Т.к. описанные выше действия выполняются редко (максимум один раз в 1-2 недели по N-раз в день), то отчего то не хотелось отдавать это ant-у и без того замусоренного.

Проблема:
Выполнить последовательность действий одним "телодвижением".

Решение:
C:\Project>svn up & ant secureClient & ant

четверг, 23 июня 2011 г.

Scale9 (slice9) для растровых изображений во Flash IDE

Преамбула:
...и это должно было произойти. Понадобилось сделать универсальную кнопку с тянущимся битмапным фоном, но не тут то было. Как оказалось на битмапы scale9 увы не работает.

Проблема:
Сделать так, чтобы мувик фона для кнопки с битмапным содержимым тянулся так же как и с векторным.

Решение:
Как зачастую бывает, помог наш добрый-умный интернет, тов. gskinner и его пост в блоге в частности. За что ему большое спасибо, gand thanks, muchos gracias, ...

Пост: http://gskinner.com/blog/archives/2010/04/bitmapslice9_sc.html
Пример: http://gskinner.com/blog/assets/BitmapSlice9/BitmapSlice9.zip
Наглядный пример:
slice9

пятница, 15 апреля 2011 г.

Использование карты прозрачности (ActionScript)

Преамбула:
Заказали как то раз сварганить флешку назначение которой было простое: обеспечить возможность всестороннего разглядывания новой упаковки, с достаточно фотореалистичным отображением. Т.к. нужен всесторонний обзор, решено было крутить-вертеть упаковку в 3D пространстве.

В качестве движка заюзал, всеми горячо любимый (особенно после нового лицензионного соглашения, разрешающего свободное его использование), движек Alternativa3D. Особенно кстати пришлась вышедшая, к началу работы над проектом, версия 7.7. Где были добавлены новые материалы, свет и проч.

Модель во флешке собирается в рантайме, т.е. подгружаются/вкоприливаются необходимые ресурсы (3DS-модель + текстуры), средствами движка парсится модель, на основе текстур создаются соответствующие материалы, все это накладывается/состыковывается.

Встала проблема с прозрачностью некоторых материалов, а именно материала покрывающего большую часть модели (упаковка полупрозрачная). В библиотеке движка есть такой полезный класс как MaterialLoader, которому нужно скормить Vector из экземпляров класса TextureMaterial или его потомков с предварительно заполненными полями (урл дифузной текстуры, урл текстуры карты прозрачности, ...). Т.к. создание материалов идет "вручную", то MaterialLoader мне показался плохим помощником. В башке зародилась мысля, а что если в качестве дифузной текстуры скормить предварительно подготовленную текстуру с наложенной картой прозрачности. Осталось дело за малым - наложить эту самую карту прозрачности.

Проблема:
Применить к текстуре карту прозрачности.

Решение:
Родился единственный метод который эту карту накладывает.
private function overlayOpacityMap(texture:BitmapData, opacityMap:BitmapData):BitmapData
{
   if (!opacityMap)
      return texture;
   
   var retval:BitmapData = new BitmapData(texture.width, texture.height, true, 0xffffffff);
   
   retval.copyPixels(texture, new Rectangle(0, 0, texture.width, texture.height), new Point());
   
   var textureColor:uint;
   var opacityColor:uint;
   for (var px:int = 0; px < retval.width; px++) 
   {
      for (var py:int = 0; py < retval.height; py++) 
      {
         textureColor = retval.getPixel32(px, py);
         opacityColor = opacityMap.getPixel(px, py);
         
         opacityColor &= 0x0000ff;
         textureColor &= 0x00ffffff;
         textureColor |= (0xff - opacityColor) << 24; 
         
         retval.setPixel32(px, py, textureColor);
      }
   }
   
   return retval;
}
В результате волшебных пассов результат получился достаточно достойным и фотореалистичным, жаль заказчик не понял всю "тяжесть, степень, глубину" и в финальной стадии разработки принял решение заменить 3D на секвенцию из 30, на мой взгляд, не лучшим образом отфотканных ракурсов.
В дополнение к выше сказанному привожу скриншот демо-проектика(used FlashDevelop) демонстрирующего наложение карты прозрачности на изображение.

В ходе написания данного поста, вспомнил, что видел где-то данный подход для эмуляции (назову её так) изображений в формате PNG с прозрачностью, и это надо сказать очень эффективно в плане трафика и места для хранения изображений аля png, по другим параметрам может и уступает.

По использованным в демо-проекте изображениям картина выглядит так:
  1. исходное изображение (jpg) + серая карта прозрачности (jpg) = 29.8 + 15.7 = 45.4 Кб
  2. исходное изображение (png) = 155.8 Кб

Простая схема защиты от "кидалова" со стороны заказчика/клиента (ActionScript)

Преамбула:
Буквально на днях закончил один прожект который делался конечному клиенту через посредника веб-студию. Т.к. с моим непосредственным заказчиком (веб-студией) я имел первый опыт (мозготрах) работы, то зародились сомнения, а не кинут ли (тьфу х3, пока такого ни разу не случалось).
В общем решил хоть как то предохранится от возможного конфуза.

Проблема:
По быстрому/простому включить в проект блокировку, в случае если клиент не расплатится за заказ в оговоренный с ним срок или прихватизирует проект на финальном этапе разработки.

Решение:
Идея думаю далеко не нова, но как говорится, свой "велосипед" ближе к телу :) (давно хочу обзавестись железным великом, да все никак да не эдак).
Решил включить код, который бы обращался по определенному адресу и запрашивал определенный ресурс и исходя из полученного результата "блокировал" флешку.
Вначале хотел запрашивать URLLоader-ом в качестве ресурса текстовый файл или php-скрипт который выдавал например слово "trusted", но из-за танцев вокруг секьюрности решил отказаться от этой затеи и воспользоватся "добрым" Lоader-ом.

Итого:
1. ресурс - gif-файл 1х1 пиксель (35 байт);
2. хостинг - публичная папка Dropbox-а (если решите воспользоватся этим сервисом, могу прислать инвайт на регпочту, обоим к положенным 2 гигам по 250 метров накинут);
3. выбрана схема блокировки по наличию ресурса, а не по его отсутствию (т.к. флешку могут просматривать локально, внутри ethernet, проблемы с хостингом и т.п.).

Реализация:
public class TrustChecker
{
   private var _stage:Stage;
   
   public function TrustChecker(stage:Stage) 
   {
      _stage = stage;
      
      var loader:Loader = new Loader();
      loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
      loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onError);
      loader.load(new URLRequest("http://myurl.com/trust.gif?" + (new Date().getTime())));
   }
   
   private function onError(e:Event):void 
   {
      var target:LoaderInfo = e.target as LoaderInfo;
      target.removeEventListener(Event.COMPLETE, onComplete);
      target.removeEventListener(IOErrorEvent.IO_ERROR, onError);
   }
   
   private function onComplete(e:Event):void 
   {
      var target:LoaderInfo = e.target as LoaderInfo;
      target.removeEventListener(Event.COMPLETE, onComplete);
      target.removeEventListener(IOErrorEvent.IO_ERROR, onError);
      
      // вот тут то можно дать волю своей фантазии для донесения мысли заказчику
      var shape:Shape = new Shape();
      shape.graphics.beginFill(0x000000);
      shape.graphics.drawRect(0, 0, _stage.stageWidth, _stage.stageHeight);
      shape.graphics.endFill();
      _stage.addChild(shape);
   }
   
}

Защита достаточно тривиальна и прошаренный заказчик её в 5 сек обойдет, отредактировав байт код или декомпилировав флеху (от этого может в какой то степени спасти укус хорошего обфускатора secureSWF).

понедельник, 21 февраля 2011 г.

Форматирование даты/времени (ActionScript)

Преамбула:
От так вот неожиданно случилось, что в приложении стало необходимо выводить дату в формате диктуемым языком интерфейса (почти как в случае с настройками локали). Проблема скажем тривиальная. Есть наверняка куча всяких форматтеров, из которых на скоряк попался флексовый (была идея портировать явовский, но там тоже как то все мутно). Но т.к. приложение на чистом ас3, то очень не хотелось таки цеплять флексовые либы и прочее, тем более флексовый форматтер юзает еще какие то классы из либ. Был бы он "сам в себе" взял бы не раздумывая, но увы и ах...

Проблема:
Применять разное форматирование даты/времени в зависимости от языковых настроек интерфейса.

Решение:
По сути требовалось форматирование даты в нашем и буржуйском формате (например: 21.02.2011 или 02/21/2011) и время в 24-часовом формате для всех языков оставить (пока).
Долго не мудрствуя родился ниже приведенный класс с несложной, но готовой к расширению, функциональностью.

public class DateFormatter
{
  private static const MAP_TOKENS:Object = new Object();
  
  private static const DATE:Date = new Date();

  /// метод форматирования №1
  public static function formatDate(date:Date, pattern:String):String
  {
    DATE.setTime(date.getTime());
    return format(DATE, pattern);
  }
  
  /// метод форматирования №2
  public static function formatTimestamp(timestamp:Number, pattern:String):String
  {
    DATE.setTime(timestamp);
    return format(DATE, pattern);
  }
  
  private static function format(date:Date, pattern:String):String
  {
    DATE.setTime(date.getTime());
    
    var tokens:Array = [];
    
    for (var token:String in MAP_TOKENS) 
    {
      if (pattern.indexOf(token) != -1)
         tokens.push(token);
    }
    
    
    var retval:String = pattern;
    for each (token in tokens) 
    {
      retval = retval.replace(token, (MAP_TOKENS[token] as Function).apply());
    }
    
    return retval;
  }
  
  private static function formatSeconds():String
  {
    return leadZero(DATE.getSeconds());
  }
  
  private static function formatMinutes():String
  {
    return leadZero(DATE.getMinutes());
  }
  
  private static function formatHours():String
  {
    return leadZero(DATE.getHours());
  }
  
  private static function formatFullYear():String
  {
    return leadZero(DATE.getFullYear());
  }
  
  private static function formatDay():String
  {
    return leadZero(DATE.getDate());
  }
  
  private static function formatMonth():String
  {
    var month:int = DATE.getMonth() + 1;
    return leadZero(month);
  }
  
  private static function leadZero(value:int):String
  {
    return (value < 10 ? "0" : "") + value.toString();
  }
  
  
  // блок статической инициализации
  {
    MAP_TOKENS["MM"] = formatMonth;
    MAP_TOKENS["DD"] = formatDay;
    MAP_TOKENS["YYYY"] = formatFullYear;
    MAP_TOKENS["hh"] = formatHours;
    MAP_TOKENS["mm"] = formatMinutes;
    MAP_TOKENS["ss"] = formatMinutes; 
  }
}
Пример использования:
var sysdate:Date = new Date();
trace(DateFormatter.formatDate(sysdate, "DD.MM.YYYY"); // 21.02.2011
trace(DateFormatter.formatDate(sysdate, "MM/DD/YYYY"); // 02/21/2011
trace(DateFormatter.formatDate(sysdate, "MM/DD/YYYY hh:mm:ss"); // 02/21/2011 14:26:01
trace(DateFormatter.formatDate("hh:mm:ss"); // 14:26:01
// или так можно, но только осторожно
trace(DateFormatter.formatDate("Today is DD.MM.YYYY"); // Today is 21.02.2011
Необходимые для конкретного языка шаблоны форматирования хранятся в соответствующих файлах. Вытягивать и хранить это все можно способом описанным в этом посте. Например:

   
 

   
    

   
 

понедельник, 7 февраля 2011 г.

Загрузка параметров приложения в зависимости от языковых настроек (ActionScript)

Преамбула:
Для мультиязыкового приложения наверняка многие пользовались классом fl.lang.Locale с помощью которого в приложение подгружаются строковые ресурсы на нужном языке. Возникла необходимость аналогично строковым, сохранять и в последствии подгружать койкакие настройки приложения в зависимости от языка, например: имя дефолтного шрифта или имя метки кадра для фона главного экрана.

Проблема:
Использование различных настроек приложения в зависимости от языка.

Решение:
За основу взят все тот же класс fl.lang.Locale, т.к. обладает всем необходимым функционалом. Более того его можно было использовать для решения данной проблемы, но я отказался от него по 2-м причинам: 1. в проекте уже используется этот класс для подгрузки строковых, 2. модель ключ-значение превращает xml-"конфиг" в одну сплошную кучу.
И так, немного поработав напильником, появился класс (не претендующий на оригинальность или что то еще в этом духе) LocaleSettings (заранее прошу прощения за мусор в комментариях, остались от Locale).

Пример xml-я который кушает этот класс:


      
         Arial
       

      
         bg_en
       

      
         BTN_ACCEPT;BTN_OK;BTN_CANCEL
       

      
         BTN_BUY;BTN_CANCEL
       


Пример использования:
const DEFAULT_LANG:String = "ru";

LocaleSettings.setDefaultLang(DEFAULT_LANG);
LocaleSettings.addXMLPath(DEFAULT_LANG, "settings/set_" + DEFAULT_LANG + ".xml"); // связываем язык с путем xml-файла

var language:String = (loaderInfo.parameters.lang ? loaderInfo.parameters.lang : DEFAULT_LANG);

LocaleSettings.addXMLPath(language, "settings/set_" + language + ".xml");
LocaleSettings.loadSettingsXML(language, languageXMLLoaded); // запускаем загрузку файла с настройками

...

private function languageXMLLoaded(success:Boolean):void
{
   if (LocaleSettings.checkXMLStatus())
   {
      // пользуем загруженные настройки
      mainScreen.setBg(LocaleSettings.loadSetting("client", "bg_frame_label");
   }
}

Примечания:
  1. Файл с настройкам для языка по умолчанию 100% должен быть (на то он и по умолчанию)
  2. В случае если для заданного языка не нашлось настоечного файла (как по причине его фактического отсутствия так и в случае ошибки в именовании языкового кода), будет загружен файл для языка по умолчанию.

четверг, 3 февраля 2011 г.

Трансляция видео mjpeg-потока (ActionScript, MJPEG)

Преамбула:
Наклевывался как то один заказик от охранной конторы. Типа забубенить плеер для показа видео с камер наблюдения с возможностью просмотра видео как в режиме он-лайн, так и в офф-лайн. В офф-лайн было все понятно, просто гнать с сервера записанное видео, проблема встала с режимом он-лайн трансляции. У заказчика использовались ip-камеры, которые отдают видео в виде mjpeg-потока и решение проблемы трансляции mjpeg-потока я таки нарыл.

Проблема:
Трансляция mjpeg-потока в flash-приложении.

Решение:
Помощники: flasher.ru, codeproject.com
package  
{
   import flash.display.*;
   import flash.events.Event;
   import flash.events.ProgressEvent;
   import flash.net.*;
   import flash.utils.*;

   public class VideoMJPEG extends Sprite
   {
      private var _stream:URLStream = new URLStream();
      private var _loader:Loader = new Loader()
      private var _buffer:ByteArray;
      private var _write:Boolean = false;   
      private var _markerDetection:Boolean = false;   
      private var _request:URLRequest = new URLRequest("http://146.176.65.10/axis-cgi/mjpg/video.cgi");

      public function VideoMJPEG()
      {
         mouseEnabled = false;
         mouseChildren = false;
         addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      }
      
      private function onAddedToStage(e:Event):void 
      {
         removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
         
         addChild(_loader);
         
         _stream.load(_request);
         _stream.addEventListener(ProgressEvent.PROGRESS, onProgress);
      }
      
      private function onProgress(event:ProgressEvent):void
      {
         var byte:int;

         // Если есть доступные байты - обработать их 
         while(_stream.bytesAvailable > 0)
         {
            byte = _stream.readUnsignedByte();
            if(_write) _buffer.writeByte(byte);
            if(_markerDetection)
            {
               switch(byte)
               {
                  case 0xD8:
                     _write = true;
                     _buffer = new ByteArray();
                     _buffer.writeByte(0xFF);
                     _buffer.writeByte(0xD8);
                  break;
                  case 0xD9:
                     _write = false;
                     _buffer.writeByte(0xD9);
                     _loader.loadBytes(_buffer);
                  break;
               }
 
            }
            _markerDetection = (byte == 0xFF);
         } 
      }
   }
}

Прим.: используемый в примере url реальный, можно прям в браузере видео-поток посмотреть.

UPD: Был как то случай с получением mjpeg-потока с камеры которая требовала базовую авторизацию, как это сделать описано в посте Доступ к ресурсам защищенным Basic authentication.

среда, 2 февраля 2011 г.

Сохранение номера ревизии в приложении (ActionScript, SVN)

Ниже описанный подход применён в контексте ActionScript-проекта находящемуся под контролем SVN. "Не ActionScript-ом единым жив программист", это подход может пригодится в других проектах с другими CSV-системами.

Проблема:
При достаточно частых обновлениях приложения в сети, хочется визуально видеть какая версия флешки перед тобой, т.е. получить подобную картинку:



Решение:
  1. Определить место, где будет сохранятся номер ревизии, например отдельный класс с настройками приложения или одно из полей уже существующего класса
    Пример класса для первого варианта:
    public class AppOption 
    {
       public static const REVISION:String = "@revision@";
    }
    
  2. Описать ant-овский task в build.xml
    <target name="buildClient">
       <!-- копируем оригинал файла где будет производится замена строки -->
       <copy file="${src}/AppOption.as" tofile="${projectDir}/AppOption.as" overwrite="true" verbose="false" />
    
       <!-- впечатываем номер ревизии по-вкусу, в данном случае последней закомиченой-->
       <property name="revision" value="COMMITTED"/>
       <exec executable="svn" outputproperty="svnlog.out">   
          <arg line="log -r ${revision} -q"/>
       </exec>
       
       <taskdef resource="net/sf/antcontrib/antcontrib.properties"/>
       <propertyregex property="revision.number" input="${svnlog.out}" select="\2">
          <regexp pattern="(r)([0-9]*)"/>
       </propertyregex>
       <replace file="${src}/AppOption.as" token="@revision@" value="${revision.number}"/>
    
    <!-- компилируем клиента -->
    
       <!-- перемещаем оригинал файла на родину -->
       <move file="${projectDir}/AppOption.as" tofile="${src}/AppOption.as" overwrite="true" verbose="false"/>
    </target>
    
  3. И наконец использовать номер ревизии
    public class Main extends Sprite 
    {
       private var _contextMenu:ContextMenu;
    
       public Main()
       {
          addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
       }
       
       private function onAddedToStage(e:Event):void
       {
          removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
          _contextMenu = new ContextMenu();
          var menuItem:ContextMenuItem = new ContextMenuItem("Version " + AppOption.REVISION);
          _contextMenu.customItems.push(menuItem);
          contextMenu = _contextMenu;
          
       }
    }
    
Номер ревизии можно использовать не только для визуального контроля версии приложения, но и в качестве антикеша ("http://anysite.com/logo.png?" + AppOption.REVISION) при подгрузке внешних ресурсов, которые могут быть изменены при публикации новой версии приложения.
P.S.: Очень полезно про автоматизацию с испольщованием ant-а см. на http://anykeytocreate.blogspot.com/2011/08/1-ant-flash-faq.html