Генерирование юзербаров и их интеграция в Icecast

в 2:50, , рубрики: icecast, perl, векторная графика, графика, КодоБред, обработка изображений, процедурная генерация, метки: , , , ,

Если кому-то надо будет нарисовать юзербар, то он откроет GIMP (или любой другой графический редактор) и нарисует в нем несколько слоев со штрихами/градиентами и отблесками. А что если надо создать сразу много юзербаров? К примеру, генерировать новую картинку при смене трека на интернет-радио? В этой статье я поделюсь небольшой методикой как это можно сделать. Сразу скажу, что вся графика у нас будет процедурной и руками не нужно ничего рисовать.

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

1. Поиск фона

Можно открыть Google Images, ввести в качестве запроса волшебное слово Nature и насладиться замечательными картинами природы. Но картин слишком много, какую выбрать? Да и какую картинку не выбери, она все равно быстро надоест. Как вариант, можно использовать несколько картинок. К примеру, я пошел в магазин с картинками и затарился там фотосетами. Осталось нарезать. Отрезать кусочек картинки легко, но какой именно? Вдруг там будет синее небо, а разве нам нужен задник из синего цвета? Что же отрезать? Мне известно 2 варианта:

  1. Выделить края (к примеру, пройтись по картинке матрицей свертки) и найдя наиболее «контрастный» регион вырезать его. Можно искать какие-то конкретные формы, скажем горизонтальные линии. Можно очень долго играть с фильтрами изображений и подбирать их последовательности, подбирая такие параметры, которые будут давать самую красивую часть картинки. Способ гибкий, но я в свое время наигрался с фильтрацией изображений и нырять в это больше не хотелось.
  2. Сжать картинку в JPEG с низким качеством, посмотреть сколько байт занимает результат. Если картинка весит немного, то скорее всего ничего интересного там нет, если картинка весит больше — значит она более детальная. Гибкости нет, зато реализовывается в 1 строчку. Этот способ я и буду использовать.

В ходе экспериментов была подобрана граница, за которой картинку не стоит считать за подходящую, равно как и обнаружено, что часть картинок имеет контур/рамочку/копирайт, которым на юзербарах делать нечего, а следовательно проще отрезать по 10% с каждой стороны, чем руками просматривать и удалять брак. Это попило у меня много времени, но в итоге появилось:

use GD;
use utf8;
opendir(mo,".");
$maxWidth=352; # ширина картинки. Кто догадается, почему такая?
$maxHeight=32; # высота картинки. Тоже не стандарт, почему?
$border=.1; #    Отрезаем по 10%
$uid=0;
@files=sort map{"".$_}grep{/jpe?g|png|gif/i}readdir(mo);
$res=GD::Image->new($maxWidth,$maxHeight,1);
$tmp=GD::Image->new($maxWidth,2000,1);
$count=0;
foreach(@files){
        print $_." (".($count++)."/".@files.")n";
        $img=GD::Image->new($_);
        next if ! defined $img;
        ($imgWidth,$imgHeight)=$img->getBounds;
        $ox=int($imgWidth*$border);
        $oy=int($imgHeight*$border);
        next if $imgWidth-$ox*2<$maxWidth || $imgHeight-$oy*2<$maxHeight;
        $aspect=$imgHeight/$imgWidth;
        $newHeight=$maxWidth*$aspect;
        next if $newHeight>2000;
        $tmp->copyResampled($img,0,0,$ox,$oy,$maxWidth,$newHeight,$imgWidth-$ox*2,$imgHeight-$oy*2);
        for($q=0;$q<=$newHeight-$maxHeight;$q+=$maxHeight){
                $res->copy($tmp,0,0,0,$q,$maxWidth,$maxHeight);
                if(length($res->jpeg(45))>=3000){ ##### Вот тут вся магия!
                        open(dd,">res/".$uid.".png");
                        print dd $res->png(9);
                        close(dd);
                        $uid++;
                        print "saved!n";
                }
        }
}

В итоге имеем примерно вот такую нарезочку: rghost.net/40299752

2. Делаем штриховку

Обычно в юзербарах штриховка под углом 45 градусов. Но как ее сделать? Рисовать самому в графическом редакторе — сложно, по крайней мере для меня. Я создал SVG-файл и отрендерив получил примерно такое:
Генерирование юзербаров и их интеграция в Icecast
Выглядит еще терпимо, но что-то мне тут не нравилось. Хотелось гибкости. А править SVG мне быстро надоело, равно как и запускать инкскейп. Согласитесь, выглядит не очень. Остается только вариант написать утильку и сгенерировать что надо. Для рисования черных и белых полосочек можно использовать формулу:

color=sin(x/3.14*step)

Но штриховка должна быть под углом, поэтому надо сделать трансформацию.

for($w=0;$w<30;$w++){
        for($q=0;$q<350;$q++){
                $rad=($angle/180*3.14159265); # Переводим градусы в радианы
                $aa=cos($rad); # а-элемент матрицы трансформации
                $cc=sin($rad); # с-элемент матрицы трансформации
                $rx=$q*$aa+$w*$cc; # Смещение с учетом трансформации
                $l=sin($rx/3.14*$step); # Получаем яркость
                $l=$l*127+128; # нормализуем
                $res->setPixel($q,$w,($a<<24) | ($l<<16) | ($l<<8) | $l);
        }
}

# Сохраняем

open(dd,">shade.png");
print dd $res->png(9);
close(dd);

image
image
Нет, это не результат, это небольшие косяки, которые вылезли при написании, но выглядит красиво. А вот примерный результат, который должен быть в итоге:
image
Угол поворота и ширину полосочек я доверил выбрать священному рандому.

3. Отблеск

Как вы уже наверное поняли, я не умею рисовать вообще, даже полосочки не смог сделать. А как рисовать отблеск? Отблеск — это попытка изобразить объем, словно на наш глянцевый юзербар сверху падает свет. Если объем, то будем использовать 3D-рендерер, пусть сам рисует. А юзербар можно будет «выпучить», представив его в виде цилиндра. Для этого я использовал Persistence of Vision Raytracer, хотя подойдет что угодно, что позволяет себя скриптовать и имеет хороший результат рендеринга.


#include "rand.inc"
#include "colors.inc"

background {color Black}
camera {location <0,0,-1.2> look_at <0,0,0>}
cylinder {<-10,0,0>,<10,0,0>,0.5 open texture{pigment{color White}}}
light_source{
        <0,1,-1>
        color White
        fade_power 3
        fade_distance 1
}
light_source{
        <0,-100,-1>
        color White
        fade_power 3
        fade_distance 0
}

Как видите, я создал цилиндр и 2 источника света. Этого достаточно для классического эффекта «отблеска», который я отчаянно пытался нарисовать, следуя пошаговым инструкциям многочисленных туториалов. А почему только 2 источника света? Почему не добавить других? В ходе экспериментов я видел что-то вроде:
image

Пришел к такому:
image

Вот такой вариант напоминает элементы интерфейса одной операционной системы:
image

4. Шрифты

image
Обычно используется шрифт Visitor, как оригинал, так и его русифицированный аналог. Но проблема в том, что на нашей радиостанции много японской музыки, как отрендерить иероглифы? Если же изначально использовать японский шрифт, то на русский/английский становиться страшно смотреть. Надо заметить, это попило больше всего крови у меня.

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


void render(char *filename,int size, int isAlias){
        fprintf(stderr,"Rendering file %s, font size: %d, is %sn",filename,size,isAlias?"anti-aliased":"mono");
        FT_Init_FreeType (&library);
        if(FT_New_Face (library, filename, 0, &face)==0){
                FT_Set_Char_Size (face, 0, size*64, 0, DPI);
                unsigned char *gl=malloc(CHARSIZE);
                int ch;
                int linepos=(LINEHEIGHT-size)/2+size-1;
                for (ch=0;ch<MAXCHARS;ch++){
                        int ind = FT_Get_Char_Index (face, ch);
                        if(ind == 0){
                                continue;
                        }
                        FT_Load_Glyph (face, ind, 0);
                        FT_Render_Glyph (face->glyph, FT_RENDER_MODE_NORMAL);
                        bits = face->glyph->bitmap;
                        if(bits.width==0 || bits.rows==0){
                                continue;
                        }
                        int oy=linepos-(face->glyph->metrics.horiBearingY>>6);
                        memset (gl,0,CHARSIZE);
                        int q,w;
                        for (w=0;w<bits.rows;w++){
                                for (q=0;q<bits.width;q++){
                                        int v=bits.buffer[bits.pitch * w+q];
                                        int y=w+oy;
                                        if(y>=0 && y<HEIGHT && q<WIDTH){
                                                if(isAlias==0){
                                                        v=v>128?255:0;
                                                }
                                                *(gl+y*WIDTH+q)=v;
                                        }
                                }
                        }
                        memcpy(font+ch*CHARSIZE,gl,CHARSIZE);
                }
                free(gl);
        }
        FT_Done_FreeType(library);
}

Здесь все просто: получаем имя файла со шрифтом, открываем его, берем первый фейс и перебираем MAXCHARS символов в нем. Далее, скармливаем ему все имеющиеся шрифты:


char *mainfonts="/usr/local/lib/X11/fonts/TrueType";
struct dirent *ent;
DIR *dir=opendir (mainfonts);
while((ent=readdir(dir))!=NULL){
        sprintf(filename,"%s/%s",mainfonts,ent->d_name);
        render (filename,15,1);
}
closedir(dir);
render("みかちゃん.ttf",15,1);
render("FZFangSong.ttf",15,1);
render("FZKaiTi.ttf",15,1);
render("LiberationMono-Regular.ttf",10,0);
render("LucidaTypewriterRegular.ttf",10,1);
render("micro/visitor1.ttf",10,0);
render("micro/Visitor_Rus.ttf",10,0);

Здесь мы все шрифты рендерим в 15 пунктов, последними же идет визитор, причем в 10 пунктов и без антиалиасинга.

На выходе получаем почти все имеющиеся глифы у нас:
Генерирование юзербаров и их интеграция в Icecast

Да, про кернинг надо забыть. Да, часть символов будет обрезана, другую часть символов будет не видно. Надо сказать, эта штука попила не мало моей крови, это лучшее что я смог родить.

5. Соединяем все вместе

backpic=loadPng(fileBackgound);
hoverpic=loadPng(fileGlossy);
shade=loadPng(fileShade);

lumaToAlpha(hoverpic);

gdImageCopy(res,backpic,0,0,0,0,350,19); // берем фон
gdImageCopy(res,shade,1,1,0,0,350,19); // добавляем штриховку
drawText(res,text,10,0); // рисуем текст с окантовкой
gdImageCopy(res,hoverpic,1,1,0,0,350,19); // блеск
gdImageRectangle(res,0,0,349,18,0); // добавляем рамочку

// сохраняем в файл
FILE *resfile=fopen(fileOut,"w");
gdImagePng(res,resfile);
fclose(resfile);

Надо сказать, что если блеск слишком яркий, то возможно имеет смысл добавлять текст уже после него. Получаем что-то вроде (текст под блеском и блеск под текстом):
Генерирование юзербаров и их интеграция в Icecast
Да, иероглифы выглядят не очень красиво, но это лучше отображения квадратиков/вопросиков.

6. Интеграция в Icecast

Можно править как административный интефейс, но я же правил непосредственно плагины. Посмотрите, какая функция назначена на plugin->set_tag? Ну и очевидная правка:

    if (strcmp (tag, "title") == 0)
    {
        free (source_mp3->url_title);
        source_mp3->url_title = value;
        userbars_render(value);
    }

Все, в userbars_render() рендерим юзербарчик и сохраняем его в директорию вебсервера. Все. Теперь на нашем радио есть модные юзербарки!

7. Послесловие

Все описанное выше — это только возможный вариант. Он не претендует ни на что, да и сами юзербары я начал делать в качестве прокрастинации, ибо от основного проекта у меня жуткая депрессия и печаль, а вот такие шалости позволяют увидеть, что я не хуже всех и хоть как-то вернуться в русло быдлокодинга. Вообще, изначально планировалось сделать анимированные юзербарки с плавающими огнями и прыгающими буквами, но у меня началась прокрастинация внутри прокрастинации, поэтому я привожу только «базовую» часть, а анимации/кернинг/оптимизации я предлагаю сделать вам самим. Делись своими экспериментами, одному человеку все равно все не придумать!

Автор: iFrolov


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js