Skip to content

How to test libass without Kodi

Stefano Gottardo edited this page Jul 15, 2022 · 4 revisions

If you need to test the libass features externally to Kodi, you can prepare a special environment in which a script will use the libass library to generate an image of the desired subtitle as a PNG. This guide is specifically for use with Windows 64bit. For the purposes of these instructions, we will use the library from upstream sources.

1. Prerequisites

To build and run the libass script, you will require:

2. Preparing the environment

Open MSYS2 shell at 64bit from the Windows menu; from now on, all following commands must be run in the shell.

Install the toolchains and required packages:

pacman -S mingw-w64-x86_64-toolchain
pacman -S libtool
pre="mingw-w64-x86_64"; pacman --noconfirm -S automake autoconf nasm make git $pre-libtool $pre-pkg-config $pre-gcc $pre-fribidi $pre-freetype $pre-harfbuzz $pre-fontconfig $pre-libpng

Clone the libass master branch:

git clone https://github.com/libass/libass.git libass-upstream

Open the new libass directory:

cd libass-upstream

Build the libass library:

./autogen.sh && ./configure && make -j 6 && make install

Return to the main directory:

cd..

Copy the libass script code (below) and save it as manual_test.c in the MSYS2 user home directory:
The path will be similar to this: C:\msys64\home\[your user name]\manual_test.c

3. Build and run the script

Compile the libass script:

gcc -O2 -g -o manual_test.exe manual_test.c -lpng $(pkg-config --static --libs libass)

Run the script:

./manual_test.exe

You should now find the rendered image in the root user directory with the name of test_output_frame.png; you can open this with any image viewer. The script uses a fixed timestamp, so the timing of the subtitles will have to be adapted according to your needs. Note that every time you edit the script file you will have to re-compile it before running again.

4. Libass test script

Show/Hide libass `manual_test.c` script source code
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

#include <ass/ass.h>
#include <ass/ass_types.h>

#include <png.h>


#define FRAME_HEIGHT 1080
#define FRAME_WIDTH  1920
#define FRAME_TIME 3700
#define FRAME_PNG_PATH "./test_output_frame.png"

struct event_src {
	const char* text;
	long start;
	long duration;
};

static const struct event_src events[] = {
	{"This a subtitle line", 3500, 2000},
	{"This is another line", 3500, 7000},
	{"Of course another one", 3500, 2000},
	{"It is clear!", 3500, 7000},
	{NULL}
};

static char ass_header[] =
	"[Script Info]\n"
	"ScriptType: v4.00+\n"
	"ScaledBorderAndShadow: yes\n"
	"YCbCr Matrix: TV.709\n"
	"PlayResX: 1920\n"
	"PlayResY: 1080\n"
	"[V4+ Styles]\n"
	"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, "
	    "Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, "
	    "Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
	"Style: Default,DejaVu Sans,72,&H00FFFFFF,&HFF0000FF,&H00303030,&H00000000,-1,-1,0,0,100,100,0,0,1,4.2,1.5,2,255,255,63,1\n"
	"[Events]\n"
	"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n";


void msg_callback(int level, const char *fmt, va_list va, void *data)
{
    printf("libass:%d: ", level);
    vprintf(fmt, va);
    printf("\n");
}

static void write_image_to_png(const char* filename, ASS_Image* img);

int main()
{
	ASS_Library* ass_library = NULL;
	ASS_Renderer* ass_renderer = NULL;
	ASS_Track* ass_track = NULL;
	int ret = 0;

	ass_library = ass_library_init();
	if(!ass_library) goto errexit;
	ass_set_message_cb(ass_library, msg_callback, NULL);
    ass_set_extract_fonts(ass_library, 1);

	ass_renderer = ass_renderer_init(ass_library);
    if (!ass_renderer) goto errexit;
	ass_set_frame_size(ass_renderer, FRAME_WIDTH, FRAME_HEIGHT);
    ass_set_fonts(ass_renderer, NULL, "Sans", 1, NULL, 1);

	ass_track = ass_new_track(ass_library);
	ass_process_codec_private(ass_track, (char*)ass_header, sizeof(ass_header));

	// Manually add events instead of process_chunk
	size_t i = 0;
	while(events[i].text) {
		int evid = ass_alloc_event(ass_track);
		if(evid < 0) {
			fprintf(stderr, "Error allocating event!\n");
			goto errexit;
		}
		ASS_Event* event = ass_track->events + evid;
		event->Start     = events[i].start;
		event->Duration  = events[i].duration;
		event->Style     = ass_track->default_style;
		event->Text      = strdup(events[i].text);
		event->ReadOrder = evid;
		if(!event->Text) {
			fprintf(stderr, "Strdup failed!\n");
			goto errexit;
		}
		fprintf(stderr, "DynEvent: %d, %d, %d, %s\n", evid, events[i].start, events[i].duration, events[i].text);
		++i;
	}

	// Make sample rendering
	ASS_Image* img = ass_render_frame(ass_renderer, ass_track, FRAME_TIME, NULL);
	if(!img) {
		fprintf(stderr, "Rendering failed!\n");
		goto errexit;
	}
	write_image_to_png(FRAME_PNG_PATH, img);


	goto clean_exit;
errexit:
	ret = 1;
clean_exit:
	if(ass_track)    ass_free_track(ass_track);
	if(ass_renderer) ass_renderer_done(ass_renderer);
    if(ass_library)  ass_library_done(ass_library);

	return ret;
}


// ----  Plucked from test/test.c  ---- //

typedef struct image_s {
    int width, height, stride;
    unsigned char *buffer;      // RGB24
} image_t;

static void write_png(const char *fname, image_t *img)
{
    FILE *fp;
    png_structp png_ptr;
    png_infop info_ptr;
    png_byte **row_pointers;
    int k;

    png_ptr =
        png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    info_ptr = png_create_info_struct(png_ptr);
    fp = NULL;

    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_write_struct(&png_ptr, &info_ptr);
        fclose(fp);
        return;
    }

    fp = fopen(fname, "wb");
    if (fp == NULL) {
        fprintf(stderr, "PNG Error opening %s for writing!\n", fname);
        return;
    }

    png_init_io(png_ptr, fp);
    png_set_compression_level(png_ptr, 0);

    png_set_IHDR(png_ptr, info_ptr, img->width, img->height,
                 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
                 PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);

    png_write_info(png_ptr, info_ptr);

    png_set_bgr(png_ptr);

    row_pointers = (png_byte **) malloc(img->height * sizeof(png_byte *));
    for (k = 0; k < img->height; k++)
        row_pointers[k] = img->buffer + img->stride * k;

    png_write_image(png_ptr, row_pointers);
    png_write_end(png_ptr, info_ptr);
    png_destroy_write_struct(&png_ptr, &info_ptr);

    free(row_pointers);

    fclose(fp);
}

static image_t *gen_image(int width, int height)
{
    image_t *img = malloc(sizeof(image_t));
    img->width = width;
    img->height = height;
    img->stride = width * 3;
    img->buffer = (unsigned char *) calloc(1, height * width * 3);
    memset(img->buffer, 63, img->stride * img->height);
    //for (int i = 0; i < height * width * 3; ++i)
    // img->buffer[i] = (i/3/50) % 100;
    return img;
}

#define _r(c)  ((c)>>24)
#define _g(c)  (((c)>>16)&0xFF)
#define _b(c)  (((c)>>8)&0xFF)
#define _a(c)  ((c)&0xFF)

static void blend_single(image_t * frame, ASS_Image *img)
{
    int x, y;
    unsigned char opacity = 255 - _a(img->color);
    unsigned char r = _r(img->color);
    unsigned char g = _g(img->color);
    unsigned char b = _b(img->color);

    unsigned char *src;
    unsigned char *dst;

    src = img->bitmap;
    dst = frame->buffer + img->dst_y * frame->stride + img->dst_x * 3;
    for (y = 0; y < img->h; ++y) {
        for (x = 0; x < img->w; ++x) {
            unsigned k = ((unsigned) src[x]) * opacity / 255;
            // possible endianness problems
            dst[x * 3] = (k * b + (255 - k) * dst[x * 3]) / 255;
            dst[x * 3 + 1] = (k * g + (255 - k) * dst[x * 3 + 1]) / 255;
            dst[x * 3 + 2] = (k * r + (255 - k) * dst[x * 3 + 2]) / 255;
        }
        src += img->stride;
        dst += frame->stride;
    }
}

static void blend(image_t * frame, ASS_Image *img)
{
    int cnt = 0;
    while (img) {
        blend_single(frame, img);
        ++cnt;
        img = img->next;
    }
    fprintf(stderr, "%d images blended\n", cnt);
}

// ----  End of "Plucked from test/test.c"  ---- //

static void write_image_to_png(const char* filename, ASS_Image* img)
{
	image_t* framebuf = gen_image(FRAME_WIDTH, FRAME_HEIGHT);
	blend(framebuf, img);
	write_png(filename, framebuf);
	free(framebuf->buffer);
	free(framebuf);
}

The sample script was provided with the courtesy of the libass developers.

5. Cleanup the enviroment

To remove libass, open the libass directory:

cd libass-upstream

Perform the uninstall:

make uninstall