BT – тулчейн баннерной разработки

Ранее я обещал рассказать о своем новом проекте, не связанном с D – выполняю обещание. Волею судеб несколько лет назад я стал профессиональным баннермейкером. Это разновидность фронтенд-разработки на стыке с анимационным дизайном – чаще всего я делаю HTML-баннеры и виджеты с разнообразной анимацией, эффектами и интерактивными механиками. Поскольку почти все рекламные сети имеют ограничение для баннеров по весу (и довольно серьезное – обычно 150 кб), эта профессия еще и пересекается в какой-то степени с демосценой, где на первом месте – искусство минимизировать информацию и генерировать ее процедурно.

Баннеры – это обычные HTML-странички, которые показывают пользователю рекламный сюжет и содержат ссылку на сайт рекламодателя. Они могут быть изготовлены при помощи самых разных инструментов, в том числе визуальных (Adobe Animate, Google Web Designer), но самое гибкое решение – писать непосредственно на HTML и JavaScript, рисуя графику либо обычными элементами DOM, либо через canvas. Благодаря отказу от Animate, вы не привязаны к его JS-библиотеке, которая сама по себе сжирает много веса, если ее нужно приложить к баннеру локально. Однако для отрисовки мало-мальски сложной анимации одним CSS вы не обойдетесь, и вам нужны такие библиотеки, как GSAP, Anime.js и др. – благо, весят они совсем немного. Основная сложность – уместить все ресурсы баннера в те самые 150 кб, что порой представляет нетривиальную задачу.

Для решения этой и многих других задач, которые возникают при разработке HTML-баннеров, я написал на Node.js комплект инструментов BT (Banner Toolchain). Его идея уходит корнями во внутренний инструментарий компании SmartHead, которым я пользовался три года и решил переделать полностью с нуля, уже в качестве независимого проекта с более эффективной реализацией большинства фич.

BT – это локальный сервер, предназначенный для разработки одностраничных сайтов с упором на минимизацию веса всех ресурсов. Например, в нем есть встроенный оптимизатор изображений, генератор CSS-анимации, функция записи баннера в GIF и видео, а также сборщик для подготовки баннера под все популярные рекламные площадки и упаковки в архив.

(далее…)

Передаем данные из GIMP в Blender

Не так давно столкнулся с интересной задачей при создании 2D-анимации в Blender: мне нужно было сделать плоскую сетку по форме объекта из PNG-изображения с прозрачным фоном. На обычную плоскость ее натянуть нельзя, так как предполагалось, что объект будет деформироваться при помощи скелета и shape keys. И таких сеток нужно было создать довольно много. Создавать их вручную, расставляя вершины по контуру картинки, как-то очень уж трудоемко – захотелось этот процесс как-то оптимизировать. И тут я вспомнил, что GIMP умеет преобразовывать маски в кривые, которые затем можно сохранить как SVG и импортировать в Blender. Осталось лишь заскриптовать эту последовательность действий!

Я решил, что переносить SVG вручную из одной программы в другую я тоже не хочу – пусть будет условно одна-единственная кнопка, по нажатию на которую слой из GIMP переносится в текущий открытый проект Blender. Подобное взаимодействие двух приложений можно реализовать при помощи технологий RPC (remote procedure call) – в частности XML-RPC, который позволяет через HTTP на клиенте вызвать серверную функцию, передав ей параметры, и затем получить результат. Преимущество XML-RPC в том, что он полностью скрывает транспортный механизм такого вызова – в скриптовых языках он выглядит просто как обычный вызов функции. Сервером я решил сделать плагин для Blender, клиентом – плагин для GIMP. Оба плагина я написал на Python, где протокол XML-RPC реализован в стандартной библиотеке. В GIMP и Blender используются разные версии Python, поэтому код работы с XML-RPC немного отличается.

Серверная часть выглядит достаточно тривиально: нужна лишь функция, которая принимает на вход строку, содержащую SVG – эта функция регистрируется как серверная функция в объекте SimpleXMLRPCServer:

import os
import bpy

import threading
import tempfile
from xmlrpc.server import SimpleXMLRPCServer

HOST = "127.0.0.1"
PORT = 8000

def svg_to_curve(svg:str):
    tmp = tempfile.NamedTemporaryFile(delete=False, mode="w")
    tmp.write(svg)
    tmp.close()
    bpy.ops.import_curve.svg(filepath=tmp.name, filter_glob='*')
    os.unlink(tmp.name)
    return {}

def launch_server():
    server = SimpleXMLRPCServer((HOST, PORT))
    server.register_function(svg_to_curve)
    server.serve_forever()

(для краткости я опустил служебный код для регистрации плагина)

Проблема возникает лишь в момент импорта SVG – Blender умеет импортировать только по файловому имени, поэтому пришлось сохранить строку во временный файл. Выглядит не очень элегантно, но работает.

На стороне GIMP делается следующее: текущему слою создается маска из альфа-канала, из маски создается выделение (gimp_image_select_item), из выделения, в свою очередь – кривая (plug_in_sel2path). Кривая экспортируется в SVG (gimp_vectors_export_to_string), а затем мы просто вызываем удаленную функцию svg_to_curve, после чего удаляем все служебные объекты.

import xmlrpclib
from gimpfu import *

def export_svg(svg):
    proxy = xmlrpclib.ServerProxy("http://localhost:8000/")
    try:
        proxy.svg_to_curve(svg)
    except xmlrpclib.Fault as err:
        pdb.gimp_message(err.faultString)

def layer_to_blender_curve(image, layer):
    if not pdb.gimp_item_is_group(layer):
        mask = layer.mask
        if not mask:
            mask = layer.create_mask(ADD_ALPHA_TRANSFER_MASK)
            layer.add_mask(mask)
        pdb.gimp_image_select_item(image, CHANNEL_OP_REPLACE, mask)
        path = pdb.plug_in_sel2path(image, None)
        pdb.gimp_selection_none(image)
        
        vector_name = pdb.gimp_path_list(image)[1][0]
        vec = pdb.gimp_image_get_vectors_by_name(image, vector_name)
        vec.name = "mask_path"
        
        svg = pdb.gimp_vectors_export_to_string(image, path)
        export_svg(svg)
        
        pdb.gimp_image_remove_vectors(image, vec)
        pdb.gimp_layer_remove_mask(layer, 0)

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

Исходники плагинов вы можете найти в репозитории https://github.com/gecko0307/image2curve.

Недостатком данного решения является то, что на стороне Blender будет постоянно работать HTTP-сервер на localhost:8000, так что вы в это время не сможете привязать к этому порту ничего другого. В Python есть способы получить случайный незанятый номер порта, чтобы не конфликтовать с другими серверами, однако в этом случае придется как-то передать порт в GIMP, что, как мне кажется, несколько усложняет весь процесс и добавляет лишнюю точку отказа.

Как ускорить загрузку изображений

Совет пользователям dlib. Не декодируйте изображение напрямую из файлового потока, это слишком медленно. Вместо этого рекомендую загрузить файл в память целиком, создать поток массива (ArrayStream) и уже его передавать в функцию-декодер:

InputStream input = openForInput("image.jpg");
ubyte[] data = New!(ubyte[])(input.size);
input.fillArray(data);
ArrayStream arrStrm = New!ArrayStream(data);
SuperImage img = loadJPEG(arrStrm);
Delete(arrStrm);
Delete(data);
input.close();

Для JPEG, например, это дает ускорение в 5-10 раз, в зависимости от размера картинки.

Сверточные фильтры в dlib с выводом в QtE

Очередной замечательный материал по dlib от LightHouse Software – продолжение серии статей по обработке изображений в dlib.

От автора:
“В этой статье мы попытаемся подружить QtE5 и dlib, сделав несложный и удобный интерфейс для накладывания фильтров на изображения, оставив при этом возможность для пользователя добавлять свои фильтры”.

http://lhs-blog.info/programming/dlang/digital-filters

Анимация в dlib

Меня внезапно осенило, как можно добавить в dlib.image поддержку анимации без существенного рефакторинга библиотеки. Классы анимированных изображений (SuperAnimatedImage) могут быть простым расширением базового класса SuperImage с возможностью хранить несколько кадров и переключаться между ними. Такие изображения полностью совместимы с любым кодом, работающим с SuperImage – чтению и записи через привычный интерфейс SuperImage подлежит срез данных, относящийся к текущему кадру.

Также естественным образом вводится класс-фабрика для создания анимированных изображений, и в результате стало возможным расширить декодер PNG до поддержки APNG:

AnimatedImageFactory imgFac = new AnimatedImageFactory;
auto res = loadPNG(openForInput("animation.apng"), imgFac);
if (res[0])
{
    SuperAnimatedImage animImg = cast(SuperAnimatedImage)res[0];
}