Skip to content

Instantly share code, notes, and snippets.

@Funami580
Created June 16, 2020 03:02
Show Gist options
  • Save Funami580/082f17a15d556844205c51d500902d00 to your computer and use it in GitHub Desktop.
Save Funami580/082f17a15d556844205c51d500902d00 to your computer and use it in GitHub Desktop.
Yutils with Linux font fix
--[[
Copyright (c) 2014, Christoph "Youka" Spanknebel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------------------------------------------------------------------------------------------------------------
Version: 14th November 2014, 15:45 (GMT+1)
Yutils
table
copy(t[, depth]) -> table
tostring(t) -> string
utf8
charrange(s, i) -> number
chars(s) -> function
len(s) -> number
math
arc_curve(x, y, cx, cy, angle) -> 8, 16, 24 or 32 numbers
bezier(pct, pts) -> number, number, number
create_matrix() -> table
get_data() -> table
set_data(matrix) -> table
identity() -> table
multiply(matrix2) -> table
translate(x, y, z) -> table
scale(x, y, z) -> table
rotate(axis, angle) -> table
inverse() -> [table]
transform(x, y, z[, w]) -> number, number, number, number
degree(x1, y1, z1, x2, y2, z2) -> number
distance(x, y[, z]) -> number
line_intersect(x0, y0, x1, y1, x2, y2, x3, y3, strict) -> number, number|nil|inf
ortho(x1, y1, z1, x2, y2, z2) -> number, number, number
randomsteps(min, max, step) -> number
round(x[, dec]) -> number
stretch(x, y, z, length) -> number, number, number
trim(x, min, max) -> number
algorithm
frames(starts, ends, dur) -> function
lines(text) -> function
shape
bounding(shape) -> number, number, number, number
detect(width, height, data[, compare_func]) -> table
filter(shape, filter) -> string
flatten(shape) -> string
glue(src_shape, dst_shape[, transform_callback]) -> string
move(shape, x, y) -> string
split(shape, max_len) -> string
to_outline(shape, width_xy[, width_y][, mode]) -> string
to_pixels(shape) -> table
transform(shape, matrix) -> string
ass
convert_time(ass_ms) -> number|string
convert_coloralpha(ass_r_a[, g, b[, a] ]) -> 1,3,4 numbers|string
interpolate_coloralpha(pct, ...) -> string
create_parser([ass_text]) -> table
parse_line(line) -> boolean
meta() -> table
styles() -> table
dialogs([extended]) -> table
decode
create_bmp_reader(filename) -> table
file_size() -> number
width() -> number
height() -> number
bit_depth() -> number
data_size() -> number
row_size() -> number
bottom_up() -> boolean
data_raw() -> string
data_packed() -> table
data_text() -> string
create_wav_reader(filename) -> table
file_size() -> number
channels_number() -> number
sample_rate() -> number
byte_rate() -> number
block_align() -> number
bits_per_sample() -> number
samples_per_channel() -> number
min_max_amplitude() -> number, number
sample_from_ms(ms) -> number
ms_from_sample(sample) -> number
position([pos]) -> number
samples_interlaced(n) -> table
samples(n) -> table
create_frequency_analyzer(samples, sample_rate) -> table
frequencies() -> table
frequency_weight(freq) -> number
frequency_range_weight(freq_min, freq_max) -> number
create_font(family, bold, italic, underline, strikeout, size[, xscale][, yscale][, hspace]) -> table
metrics() -> table
text_extents(text) -> table
text_to_shape(text) -> string
list_fonts([with_filenames]) -> table
]]
-- Configuration
local FP_PRECISION = 3 -- Floating point precision by numbers behind point (for shape points)
local CURVE_TOLERANCE = 1 -- Angle in degree to define a curve as flat
local MAX_CIRCUMFERENCE = 1.5 -- Circumference step size to create round edges out of lines
local MITER_LIMIT = 200 -- Maximal length of a miter join
local SUPERSAMPLING = 8 -- Anti-aliasing precision for shape to pixels conversion
local FONT_PRECISION = 64 -- Font scale for better precision output from native font system
local LIBASS_FONTHACK = true -- Scale font data to fontsize? (no effect on windows)
local LIBPNG_PATH = "libpng" -- libpng dynamic library location or shortcut (for system library loading function)
-- Load FFI interface
local ffi = require("ffi")
-- Check OS & load fitting system libraries
local advapi, pangocairo, fontconfig
if ffi.os == "Windows" then
-- WinGDI already loaded in C namespace by default
-- Load advanced winapi library
advapi = ffi.load("Advapi32")
-- Set C definitions for WinAPI
ffi.cdef([[
enum{CP_UTF8 = 65001};
enum{MM_TEXT = 1};
enum{TRANSPARENT = 1};
enum{
FW_NORMAL = 400,
FW_BOLD = 700
};
enum{DEFAULT_CHARSET = 1};
enum{OUT_TT_PRECIS = 4};
enum{CLIP_DEFAULT_PRECIS = 0};
enum{ANTIALIASED_QUALITY = 4};
enum{DEFAULT_PITCH = 0x0};
enum{FF_DONTCARE = 0x0};
enum{
PT_MOVETO = 0x6,
PT_LINETO = 0x2,
PT_BEZIERTO = 0x4,
PT_CLOSEFIGURE = 0x1
};
typedef unsigned int UINT;
typedef unsigned long DWORD;
typedef DWORD* LPDWORD;
typedef const char* LPCSTR;
typedef const wchar_t* LPCWSTR;
typedef wchar_t* LPWSTR;
typedef char* LPSTR;
typedef void* HANDLE;
typedef HANDLE HDC;
typedef int BOOL;
typedef BOOL* LPBOOL;
typedef unsigned int size_t;
typedef HANDLE HFONT;
typedef HANDLE HGDIOBJ;
typedef long LONG;
typedef wchar_t WCHAR;
typedef unsigned char BYTE;
typedef BYTE* LPBYTE;
typedef int INT;
typedef long LPARAM;
static const int LF_FACESIZE = 32;
static const int LF_FULLFACESIZE = 64;
typedef struct{
LONG tmHeight;
LONG tmAscent;
LONG tmDescent;
LONG tmInternalLeading;
LONG tmExternalLeading;
LONG tmAveCharWidth;
LONG tmMaxCharWidth;
LONG tmWeight;
LONG tmOverhang;
LONG tmDigitizedAspectX;
LONG tmDigitizedAspectY;
WCHAR tmFirstChar;
WCHAR tmLastChar;
WCHAR tmDefaultChar;
WCHAR tmBreakChar;
BYTE tmItalic;
BYTE tmUnderlined;
BYTE tmStruckOut;
BYTE tmPitchAndFamily;
BYTE tmCharSet;
}TEXTMETRICW, *LPTEXTMETRICW;
typedef struct{
LONG cx;
LONG cy;
}SIZE, *LPSIZE;
typedef struct{
LONG left;
LONG top;
LONG right;
LONG bottom;
}RECT;
typedef const RECT* LPCRECT;
typedef struct{
LONG x;
LONG y;
}POINT, *LPPOINT;
typedef struct{
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
WCHAR lfFaceName[LF_FACESIZE];
}LOGFONTW, *LPLOGFONTW;
typedef struct{
LOGFONTW elfLogFont;
WCHAR elfFullName[LF_FULLFACESIZE];
WCHAR elfStyle[LF_FACESIZE];
WCHAR elfScript[LF_FACESIZE];
}ENUMLOGFONTEXW, *LPENUMLOGFONTEXW;
enum{
FONTTYPE_RASTER = 1,
FONTTYPE_DEVICE = 2,
FONTTYPE_TRUETYPE = 4
};
typedef int (__stdcall *FONTENUMPROC)(const ENUMLOGFONTEXW*, const void*, DWORD, LPARAM);
enum{ERROR_SUCCESS = 0};
typedef HANDLE HKEY;
typedef HKEY* PHKEY;
enum{HKEY_LOCAL_MACHINE = 0x80000002};
typedef enum{KEY_READ = 0x20019}REGSAM;
int MultiByteToWideChar(UINT, DWORD, LPCSTR, int, LPWSTR, int);
int WideCharToMultiByte(UINT, DWORD, LPCWSTR, int, LPSTR, int, LPCSTR, LPBOOL);
HDC CreateCompatibleDC(HDC);
BOOL DeleteDC(HDC);
int SetMapMode(HDC, int);
int SetBkMode(HDC, int);
size_t wcslen(const wchar_t*);
HFONT CreateFontW(int, int, int, int, int, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, LPCWSTR);
HGDIOBJ SelectObject(HDC, HGDIOBJ);
BOOL DeleteObject(HGDIOBJ);
BOOL GetTextMetricsW(HDC, LPTEXTMETRICW);
BOOL GetTextExtentPoint32W(HDC, LPCWSTR, int, LPSIZE);
BOOL BeginPath(HDC);
BOOL ExtTextOutW(HDC, int, int, UINT, LPCRECT, LPCWSTR, UINT, const INT*);
BOOL EndPath(HDC);
int GetPath(HDC, LPPOINT, LPBYTE, int);
BOOL AbortPath(HDC);
int EnumFontFamiliesExW(HDC, LPLOGFONTW, FONTENUMPROC, LPARAM, DWORD);
LONG RegOpenKeyExA(HKEY, LPCSTR, DWORD, REGSAM, PHKEY);
LONG RegCloseKey(HKEY);
LONG RegEnumValueW(HKEY, DWORD, LPWSTR, LPDWORD, LPDWORD, LPDWORD, LPBYTE, LPDWORD);
]])
else -- Unix
-- Attempt to load pangocairo library
pcall(function()
pangocairo = ffi.load("pangocairo-1.0.so") -- Extension must be appended because of dot already in filename
-- Set C definitions for pangocairo
ffi.cdef([[
typedef enum{
CAIRO_FORMAT_INVALID = -1,
CAIRO_FORMAT_ARGB32 = 0,
CAIRO_FORMAT_RGB24 = 1,
CAIRO_FORMAT_A8 = 2,
CAIRO_FORMAT_A1 = 3,
CAIRO_FORMAT_RGB16_565 = 4,
CAIRO_FORMAT_RGB30 = 5
}cairo_format_t;
typedef void cairo_surface_t;
typedef void cairo_t;
typedef void PangoLayout;
typedef void* gpointer;
static const int PANGO_SCALE = 1024;
typedef void PangoFontDescription;
typedef enum{
PANGO_WEIGHT_THIN = 100,
PANGO_WEIGHT_ULTRALIGHT = 200,
PANGO_WEIGHT_LIGHT = 300,
PANGO_WEIGHT_NORMAL = 400,
PANGO_WEIGHT_MEDIUM = 500,
PANGO_WEIGHT_SEMIBOLD = 600,
PANGO_WEIGHT_BOLD = 700,
PANGO_WEIGHT_ULTRABOLD = 800,
PANGO_WEIGHT_HEAVY = 900,
PANGO_WEIGHT_ULTRAHEAVY = 1000
}PangoWeight;
typedef enum{
PANGO_STYLE_NORMAL,
PANGO_STYLE_OBLIQUE,
PANGO_STYLE_ITALIC
}PangoStyle;
typedef void PangoAttrList;
typedef void PangoAttribute;
typedef enum{
PANGO_UNDERLINE_NONE,
PANGO_UNDERLINE_SINGLE,
PANGO_UNDERLINE_DOUBLE,
PANGO_UNDERLINE_LOW,
PANGO_UNDERLINE_ERROR
}PangoUnderline;
typedef int gint;
typedef gint gboolean;
typedef void PangoContext;
typedef unsigned int guint;
typedef struct{
guint ref_count;
int ascent;
int descent;
int approximate_char_width;
int approximate_digit_width;
int underline_position;
int underline_thickness;
int strikethrough_position;
int strikethrough_thickness;
}PangoFontMetrics;
typedef void PangoLanguage;
typedef struct{
int x;
int y;
int width;
int height;
}PangoRectangle;
typedef enum{
CAIRO_STATUS_SUCCESS = 0
}cairo_status_t;
typedef enum{
CAIRO_PATH_MOVE_TO,
CAIRO_PATH_LINE_TO,
CAIRO_PATH_CURVE_TO,
CAIRO_PATH_CLOSE_PATH
}cairo_path_data_type_t;
typedef union{
struct{
cairo_path_data_type_t type;
int length;
}header;
struct{
double x, y;
}point;
}cairo_path_data_t;
typedef struct{
cairo_status_t status;
cairo_path_data_t* data;
int num_data;
}cairo_path_t;
cairo_surface_t* cairo_image_surface_create(cairo_format_t, int, int);
void cairo_surface_destroy(cairo_surface_t*);
cairo_t* cairo_create(cairo_surface_t*);
void cairo_destroy(cairo_t*);
PangoLayout* pango_cairo_create_layout(cairo_t*);
void g_object_unref(gpointer);
PangoFontDescription* pango_font_description_new(void);
void pango_font_description_free(PangoFontDescription*);
void pango_font_description_set_family(PangoFontDescription*, const char*);
void pango_font_description_set_weight(PangoFontDescription*, PangoWeight);
void pango_font_description_set_style(PangoFontDescription*, PangoStyle);
void pango_font_description_set_absolute_size(PangoFontDescription*, double);
void pango_layout_set_font_description(PangoLayout*, PangoFontDescription*);
PangoAttrList* pango_attr_list_new(void);
void pango_attr_list_unref(PangoAttrList*);
void pango_attr_list_insert(PangoAttrList*, PangoAttribute*);
PangoAttribute* pango_attr_underline_new(PangoUnderline);
PangoAttribute* pango_attr_strikethrough_new(gboolean);
PangoAttribute* pango_attr_letter_spacing_new(int);
void pango_layout_set_attributes(PangoLayout*, PangoAttrList*);
PangoContext* pango_layout_get_context(PangoLayout*);
const PangoFontDescription* pango_layout_get_font_description(PangoLayout*);
PangoFontMetrics* pango_context_get_metrics(PangoContext*, const PangoFontDescription*, PangoLanguage*);
void pango_font_metrics_unref(PangoFontMetrics*);
int pango_font_metrics_get_ascent(PangoFontMetrics*);
int pango_font_metrics_get_descent(PangoFontMetrics*);
int pango_layout_get_spacing(PangoLayout*);
void pango_layout_set_text(PangoLayout*, const char*, int);
void pango_layout_get_pixel_extents(PangoLayout*, PangoRectangle*, PangoRectangle*);
void cairo_save(cairo_t*);
void cairo_restore(cairo_t*);
void cairo_scale(cairo_t*, double, double);
void pango_cairo_layout_path(cairo_t*, PangoLayout*);
void cairo_new_path(cairo_t*);
cairo_path_t* cairo_copy_path(cairo_t*);
void cairo_path_destroy(cairo_path_t*);
]])
end)
-- Attempt to load fontconfig library
pcall(function()
fontconfig = ffi.load("fontconfig")
-- Set C definitions for fontconfig
ffi.cdef([[
typedef void FcConfig;
typedef void FcPattern;
typedef struct{
int nobject;
int sobject;
const char** objects;
}FcObjectSet;
typedef struct{
int nfont;
int sfont;
FcPattern** fonts;
}FcFontSet;
typedef enum{
FcResultMatch,
FcResultNoMatch,
FcResultTypeMismatch,
FcResultNoId,
FcResultOutOfMemory
}FcResult;
typedef unsigned char FcChar8;
typedef int FcBool;
FcConfig* FcInitLoadConfigAndFonts(void);
FcPattern* FcPatternCreate(void);
void FcPatternDestroy(FcPattern*);
FcObjectSet* FcObjectSetBuild(const char*, ...);
void FcObjectSetDestroy(FcObjectSet*);
FcFontSet* FcFontList(FcConfig*, FcPattern*, FcObjectSet*);
void FcFontSetDestroy(FcFontSet*);
FcResult FcPatternGetString(FcPattern*, const char*, int, FcChar8**);
FcResult FcPatternGetBool(FcPattern*, const char*, int, FcBool*);
]])
end)
end
-- Load PNG decode library (at least try it)
local libpng
pcall(function()
libpng = ffi.load(LIBPNG_PATH)
-- Set C definitions for libpng
ffi.cdef([[
static const int PNG_SIGNATURE_SIZE = 8;
typedef unsigned char png_byte;
typedef png_byte* png_bytep;
typedef const png_bytep png_const_bytep;
typedef unsigned int png_size_t;
typedef char png_char;
typedef png_char* png_charp;
typedef const png_charp png_const_charp;
typedef void png_void;
typedef png_void* png_voidp;
typedef struct png_struct* png_structp;
typedef const png_structp png_const_structp;
typedef struct png_info* png_infop;
typedef const png_infop png_const_infop;
typedef unsigned int png_uint_32;
typedef void (__cdecl *png_error_ptr)(png_structp, png_const_charp);
typedef void (__cdecl *png_rw_ptr)(png_structp, png_bytep, png_size_t);
enum{
PNG_TRANSFORM_STRIP_16 = 0x1,
PNG_TRANSFORM_PACKING = 0x4,
PNG_TRANSFORM_EXPAND = 0x10,
PNG_TRANSFORM_BGR = 0x80
};
enum{
PNG_COLOR_MASK_COLOR = 2,
PNG_COLOR_MASK_ALPHA = 4
};
enum{
PNG_COLOR_TYPE_RGB = PNG_COLOR_MASK_COLOR,
PNG_COLOR_TYPE_RGBA = PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_ALPHA
};
void* memcpy(void*, const void*, size_t);
int png_sig_cmp(png_const_bytep, png_size_t, png_size_t);
png_structp png_create_read_struct(png_const_charp, png_voidp, png_error_ptr, png_error_ptr);
void png_destroy_read_struct(png_structp*, png_infop*, png_infop*);
png_infop png_create_info_struct(png_structp);
void png_set_read_fn(png_structp, png_voidp, png_rw_ptr);
void png_read_png(png_structp, png_infop, int, png_voidp);
int png_set_interlace_handling(png_structp);
void png_read_update_info(png_structp, png_infop);
png_uint_32 png_get_image_width(png_const_structp, png_const_infop);
png_uint_32 png_get_image_height(png_const_structp, png_const_infop);
png_byte png_get_color_type(png_const_structp, png_const_infop);
png_size_t png_get_rowbytes(png_const_structp, png_const_infop);
png_bytep* png_get_rows(png_const_structp, png_const_infop);
]])
end)
-- Helper functions
local unpack = table.unpack or unpack
local function rotate2d(x, y, angle)
local ra = math.rad(angle)
return math.cos(ra)*x - math.sin(ra)*y,
math.sin(ra)*x + math.cos(ra)*y
end
local function bton(s)
-- Get numeric presentation (=byte) of string characters
local bytes, n = {s:byte(1,-1)}, 0
-- Combine bytes to unsigned integer number
for i = 0, #s-1 do
n = n + bytes[1+i] * 256^i
end
return n
end
local function utf8_to_utf16(s)
-- Get resulting utf16 characters number (+ null-termination)
local wlen = ffi.C.MultiByteToWideChar(ffi.C.CP_UTF8, 0x0, s, -1, nil, 0)
-- Allocate array for utf16 characters storage
local ws = ffi.new("wchar_t[?]", wlen)
-- Convert utf8 string to utf16 characters
ffi.C.MultiByteToWideChar(ffi.C.CP_UTF8, 0x0, s, -1, ws, wlen)
-- Return utf16 C string
return ws
end
local function utf16_to_utf8(ws)
-- Get resulting utf8 characters number (+ null-termination)
local slen = ffi.C.WideCharToMultiByte(ffi.C.CP_UTF8, 0x0, ws, -1, nil, 0, nil, nil)
-- Allocate array for utf8 characters storage
local s = ffi.new("char[?]", slen)
-- Convert utf16 string to utf8 characters
ffi.C.WideCharToMultiByte(ffi.C.CP_UTF8, 0x0, ws, -1, s, slen, nil, nil)
-- Return utf8 Lua string
return ffi.string(s)
end
-- Create library table
local Yutils
Yutils = {
-- Table sublibrary
table = {
-- Copies table deep
copy = function(t, depth)
-- Check argument
if type(t) ~= "table" or depth ~= nil and not(type(depth) == "number" and depth >= 1) then
error("table and optional depth expected", 2)
end
-- Copy & return
local function copy_recursive(old_t)
local new_t = {}
for key, value in pairs(old_t) do
new_t[key] = type(value) == "table" and copy_recursive(value) or value
end
return new_t
end
local function copy_recursive_n(old_t, depth)
local new_t = {}
for key, value in pairs(old_t) do
new_t[key] = type(value) == "table" and depth >= 2 and copy_recursive_n(value, depth-1) or value
end
return new_t
end
return depth and copy_recursive_n(t, depth) or copy_recursive(t)
end,
-- Converts table to string
tostring = function(t)
-- Check argument
if type(t) ~= "table" then
error("table expected", 2)
end
-- Result storage
local result, result_n = {}, 0
-- Convert to string!
local function convert_recursive(t, space)
for key, value in pairs(t) do
if type(key) == "string" then
key = string.format("%q", key)
end
if type(value) == "string" then
value = string.format("%q", value)
end
result_n = result_n + 1
result[result_n] = string.format("%s[%s] = %s", space, key, value)
if type(value) == "table" then
convert_recursive(value, space .. "\t")
end
end
end
convert_recursive(t, "")
-- Return result as string
return table.concat(result, "\n")
end
},
-- UTF8 sublibrary
utf8 = {
--[[
UTF32 -> UTF8
--------------
U-00000000 - …U-0000007F: 0xxxxxxx
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
]]
-- UTF8 character range at string codepoint
charrange = function(s, i)
-- Check arguments
if type(s) ~= "string" or type(i) ~= "number" or i < 1 or i > #s then
error("string and string index expected", 2)
end
-- Evaluate codepoint to range
local byte = s:byte(i)
return not byte and 0 or
byte < 192 and 1 or
byte < 224 and 2 or
byte < 240 and 3 or
byte < 248 and 4 or
byte < 252 and 5 or
6
end,
-- Creates iterator through UTF8 characters
chars = function(s)
-- Check argument
if type(s) ~= "string" then
error("string expected", 2)
end
-- Return utf8 characters iterator
local char_i, s_pos, s_len = 0, 1, #s
return function()
if s_pos <= s_len then
local cur_pos = s_pos
s_pos = s_pos + Yutils.utf8.charrange(s, s_pos)
if s_pos-1 <= s_len then
char_i = char_i + 1
return char_i, s:sub(cur_pos, s_pos-1)
end
end
end
end,
-- Get UTF8 characters number in string
len = function(s)
-- Check argument
if type(s) ~= "string" then
error("string expected", 2)
end
-- Count UTF8 characters
local n = 0
for _ in Yutils.utf8.chars(s) do
n = n + 1
end
return n
end
},
-- Math sublibrary
math = {
-- Converts an arc to 1-4 cubic bezier curve(s)
arc_curve = function(x, y, cx, cy, angle)
-- Check arguments
if type(x) ~= "number" or type(y) ~= "number" or type(cx) ~= "number" or type(cy) ~= "number" or type(angle) ~= "number" or
angle < -360 or angle > 360 then
error("start & center point and valid angle (-360<=x<=360) expected", 2)
end
-- Something to do?
if angle ~= 0 then
-- Factor for bezier control points distance to node points
local kappa = 4 * (math.sqrt(2) - 1) / 3
-- Relative points to center
local rx0, ry0, rx1, ry1, rx2, ry2, rx3, ry3, rx03, ry03 = x - cx, y - cy
-- Define arc clock direction & set angle to positive range
local cw = angle > 0 and 1 or -1
if angle < 0 then
angle = -angle
end
-- Create curves in 90 degree chunks
local curves, curves_n, angle_sum, cur_angle_pct = {}, 0, 0
repeat
-- Get arc end point
cur_angle_pct = math.min(angle - angle_sum, 90) / 90
rx3, ry3 = rotate2d(rx0, ry0, cw * 90 * cur_angle_pct)
-- Get arc start to end vector
rx03, ry03 = rx3 - rx0, ry3 - ry0
-- Scale arc vector to curve node <-> control point distance
rx03, ry03 = Yutils.math.stretch(rx03, ry03, 0, math.sqrt(Yutils.math.distance(rx03, ry03)^2/2) * kappa)
-- Get curve control points
rx1, ry1 = rotate2d(rx03, ry03, cw * -45 * cur_angle_pct)
rx1, ry1 = rx0 + rx1, ry0 + ry1
rx2, ry2 = rotate2d(-rx03, -ry03, cw * 45 * cur_angle_pct)
rx2, ry2 = rx3 + rx2, ry3 + ry2
-- Insert curve to output
curves[curves_n+1], curves[curves_n+2], curves[curves_n+3], curves[curves_n+4],
curves[curves_n+5], curves[curves_n+6], curves[curves_n+7], curves[curves_n+8] =
cx + rx0, cy + ry0, cx + rx1, cy + ry1, cx + rx2, cy + ry2, cx + rx3, cy + ry3
curves_n = curves_n + 8
-- Prepare next curve
rx0, ry0 = rx3, ry3
angle_sum = angle_sum + 90
until angle_sum >= angle
-- Return curve points as tuple
return unpack(curves)
end
end,
-- Get point on n-degree bezier curve
bezier = function(pct, pts)
-- Check arguments
if type(pct) ~= "number" or pct < 0 or pct > 1 or type(pts) ~= "table" then
error("percent number and points table expected", 2)
end
local pts_n = #pts
if pts_n < 2 then
error("at least 2 points expected", 2)
end
for _, value in ipairs(pts) do
if type(value[1]) ~= "number" or type(value[2]) ~= "number" or (value[3] ~= nil and type(value[3]) ~= "number") then
error("points have to be tables with 2 or 3 numbers", 2)
end
end
-- Pick a fitting fast calculation
local pct_inv = 1 - pct
if pts_n == 2 then -- Linear curve
return pct_inv * pts[1][1] + pct * pts[2][1],
pct_inv * pts[1][2] + pct * pts[2][2],
pts[1][3] and pts[2][3] and pct_inv * pts[1][3] + pct * pts[2][3] or 0
elseif pts_n == 3 then -- Quadratic curve
return pct_inv * pct_inv * pts[1][1] + 2 * pct_inv * pct * pts[2][1] + pct * pct * pts[3][1],
pct_inv * pct_inv * pts[1][2] + 2 * pct_inv * pct * pts[2][2] + pct * pct * pts[3][2],
pts[1][3] and pts[2][3] and pct_inv * pct_inv * pts[1][3] + 2 * pct_inv * pct * pts[2][3] + pct * pct * pts[3][3] or 0
elseif pts_n == 4 then -- Cubic curve
return pct_inv * pct_inv * pct_inv * pts[1][1] + 3 * pct_inv * pct_inv * pct * pts[2][1] + 3 * pct_inv * pct * pct * pts[3][1] + pct * pct * pct * pts[4][1],
pct_inv * pct_inv * pct_inv * pts[1][2] + 3 * pct_inv * pct_inv * pct * pts[2][2] + 3 * pct_inv * pct * pct * pts[3][2] + pct * pct * pct * pts[4][2],
pts[1][3] and pts[2][3] and pts[3][3] and pts[4][3] and pct_inv * pct_inv * pct_inv * pts[1][3] + 3 * pct_inv * pct_inv * pct * pts[2][3] + 3 * pct_inv * pct * pct * pts[3][3] + pct * pct * pct * pts[4][3] or 0
else -- pts_n > 4
-- Factorial
local function fac(n)
local k = 1
for i=2, n do
k = k * i
end
return k
end
-- Calculate coordinate
local ret_x, ret_y, ret_z = 0, 0, 0
local n, bern, pt = pts_n - 1
for i=0, n do
pt = pts[1+i]
-- Bernstein polynom
bern = fac(n) / (fac(i) * fac(n - i)) * --Binomial coefficient
pct^i * pct_inv^(n - i)
ret_x = ret_x + pt[1] * bern
ret_y = ret_y + pt[2] * bern
ret_z = ret_z + (pt[3] or 0) * bern
end
return ret_x, ret_y, ret_z
end
end,
-- Creates 3d matrix
create_matrix = function()
-- Matrix data
local matrix = {1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1}
-- Matrix object
local obj
obj = {
-- Get matrix data
get_data = function()
return Yutils.table.copy(matrix)
end,
-- Set matrix data
set_data = function(new_matrix)
-- Check arguments
if type(new_matrix) ~= "table" or #new_matrix ~= 16 then
error("4x4 matrix expected", 2)
end
for _, value in ipairs(new_matrix) do
if type(value) ~= "number" then
error("matrix must contain only numbers", 2)
end
end
-- Replace old matrix
matrix = Yutils.table.copy(new_matrix)
-- Return this object
return obj
end,
-- Set matrix to identity
identity = function()
-- Set matrix to default / no transformation
matrix[1] = 1
matrix[2] = 0
matrix[3] = 0
matrix[4] = 0
matrix[5] = 0
matrix[6] = 1
matrix[7] = 0
matrix[8] = 0
matrix[9] = 0
matrix[10] = 0
matrix[11] = 1
matrix[12] = 0
matrix[13] = 0
matrix[14] = 0
matrix[15] = 0
matrix[16] = 1
-- Return this object
return obj
end,
-- Multiplies matrix with given one
multiply = function(matrix2)
-- Check arguments
if type(matrix2) ~= "table" or #matrix2 ~= 16 then
error("4x4 matrix expected", 2)
end
for _, value in ipairs(matrix2) do
if type(value) ~= "number" then
error("matrix must contain only numbers", 2)
end
end
-- Multipy matrices to create new one
local new_matrix = {0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0}
for i=1, 16 do
for j=0, 3 do
new_matrix[i] = new_matrix[i] + matrix[1 + (i-1) % 4 + j * 4] * matrix2[1 + math.floor((i-1) / 4) * 4 + j]
end
end
-- Replace old matrix with multiply result
matrix = new_matrix
-- Return this object
return obj
end,
-- Applies translation to matrix
translate = function(x, y, z)
-- Check arguments
if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then
error("3 translation values expected", 2)
end
-- Add translation to matrix
obj.multiply({1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1})
-- Return this object
return obj
end,
-- Applies scale to matrix
scale = function(x, y, z)
-- Check arguments
if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then
error("3 scale factors expected", 2)
end
-- Add scale to matrix
obj.multiply({x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1})
-- Return this object
return obj
end,
-- Applies rotation to matrix
rotate = function(axis, angle)
-- Check arguments
if (axis ~= "x" and axis ~= "y" and axis ~= "z") or type(angle) ~= "number" then
error("axis (as string) and angle (in degree) expected", 2)
end
-- Convert angle from degree to radian
angle = math.rad(angle)
-- Rotate by axis
if axis == "x" then
obj.multiply({1, 0, 0, 0,
0, math.cos(angle), -math.sin(angle), 0,
0, math.sin(angle), math.cos(angle), 0,
0, 0, 0, 1})
elseif axis == "y" then
obj.multiply({math.cos(angle), 0, math.sin(angle), 0,
0, 1, 0, 0,
-math.sin(angle), 0, math.cos(angle), 0,
0, 0, 0, 1})
else -- axis == "z"
obj.multiply({math.cos(angle), -math.sin(angle), 0, 0,
math.sin(angle), math.cos(angle), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1})
end
-- Return this object
return obj
end,
-- Inverses matrix
inverse = function()
-- Create inversion matrix
local inv_matrix = {
matrix[6] * matrix[11] * matrix[16] - matrix[6] * matrix[15] * matrix[12] - matrix[7] * matrix[10] * matrix[16] + matrix[7] * matrix[14] * matrix[12] +matrix[8] * matrix[10] * matrix[15] - matrix[8] * matrix[14] * matrix[11],
-matrix[2] * matrix[11] * matrix[16] + matrix[2] * matrix[15] * matrix[12] + matrix[3] * matrix[10] * matrix[16] - matrix[3] * matrix[14] * matrix[12] - matrix[4] * matrix[10] * matrix[15] + matrix[4] * matrix[14] * matrix[11],
matrix[2] * matrix[7] * matrix[16] - matrix[2] * matrix[15] * matrix[8] - matrix[3] * matrix[6] * matrix[16] + matrix[3] * matrix[14] * matrix[8] + matrix[4] * matrix[6] * matrix[15] - matrix[4] * matrix[14] * matrix[7],
-matrix[2] * matrix[7] * matrix[12] + matrix[2] * matrix[11] * matrix[8] +matrix[3] * matrix[6] * matrix[12] - matrix[3] * matrix[10] * matrix[8] - matrix[4] * matrix[6] * matrix[11] + matrix[4] * matrix[10] * matrix[7],
-matrix[5] * matrix[11] * matrix[16] + matrix[5] * matrix[15] * matrix[12] + matrix[7] * matrix[9] * matrix[16] - matrix[7] * matrix[13] * matrix[12] - matrix[8] * matrix[9] * matrix[15] + matrix[8] * matrix[13] * matrix[11],
matrix[1] * matrix[11] * matrix[16] - matrix[1] * matrix[15] * matrix[12] - matrix[3] * matrix[9] * matrix[16] + matrix[3] * matrix[13] * matrix[12] + matrix[4] * matrix[9] * matrix[15] - matrix[4] * matrix[13] * matrix[11],
-matrix[1] * matrix[7] * matrix[16] + matrix[1] * matrix[15] * matrix[8] + matrix[3] * matrix[5] * matrix[16] - matrix[3] * matrix[13] * matrix[8] - matrix[4] * matrix[5] * matrix[15] + matrix[4] * matrix[13] * matrix[7],
matrix[1] * matrix[7] * matrix[12] - matrix[1] * matrix[11] * matrix[8] - matrix[3] * matrix[5] * matrix[12] + matrix[3] * matrix[9] * matrix[8] + matrix[4] * matrix[5] * matrix[11] - matrix[4] * matrix[9] * matrix[7],
matrix[5] * matrix[10] * matrix[16] - matrix[5] * matrix[14] * matrix[12] - matrix[6] * matrix[9] * matrix[16] + matrix[6] * matrix[13] * matrix[12] + matrix[8] * matrix[9] * matrix[14] - matrix[8] * matrix[13] * matrix[10],
-matrix[1] * matrix[10] * matrix[16] + matrix[1] * matrix[14] * matrix[12] + matrix[2] * matrix[9] * matrix[16] - matrix[2] * matrix[13] * matrix[12] - matrix[4] * matrix[9] * matrix[14] + matrix[4] * matrix[13] * matrix[10],
matrix[1] * matrix[6] * matrix[16] - matrix[1] * matrix[14] * matrix[8] - matrix[2] * matrix[5] * matrix[16] + matrix[2] * matrix[13] * matrix[8] + matrix[4] * matrix[5] * matrix[14] - matrix[4] * matrix[13] * matrix[6],
-matrix[1] * matrix[6] * matrix[12] + matrix[1] * matrix[10] * matrix[8] + matrix[2] * matrix[5] * matrix[12] - matrix[2] * matrix[9] * matrix[8] - matrix[4] * matrix[5] * matrix[10] + matrix[4] * matrix[9] * matrix[6],
-matrix[5] * matrix[10] * matrix[15] + matrix[5] * matrix[14] * matrix[11] + matrix[6] * matrix[9] * matrix[15] - matrix[6] * matrix[13] * matrix[11] - matrix[7] * matrix[9] * matrix[14] + matrix[7] * matrix[13] * matrix[10],
matrix[1] * matrix[10] * matrix[15] - matrix[1] * matrix[14] * matrix[11] - matrix[2] * matrix[9] * matrix[15] + matrix[2] * matrix[13] * matrix[11] + matrix[3] * matrix[9] * matrix[14] - matrix[3] * matrix[13] * matrix[10],
-matrix[1] * matrix[6] * matrix[15] + matrix[1] * matrix[14] * matrix[7] + matrix[2] * matrix[5] * matrix[15] - matrix[2] * matrix[13] * matrix[7] - matrix[3] * matrix[5] * matrix[14] + matrix[3] * matrix[13] * matrix[6],
matrix[1] * matrix[6] * matrix[11] - matrix[1] * matrix[10] * matrix[7] - matrix[2] * matrix[5] * matrix[11] + matrix[2] * matrix[9] * matrix[7] + matrix[3] * matrix[5] * matrix[10] - matrix[3] * matrix[9] * matrix[6]
}
-- Calculate determinant
local det = matrix[1] * inv_matrix[1] +
matrix[5] * inv_matrix[2] +
matrix[9] * inv_matrix[3] +
matrix[13] * inv_matrix[4]
-- Matrix inversion possible?
if det ~= 0 then
-- Invert matrix
det = 1 / det
for i=1, 16 do
matrix[i] = inv_matrix[i] * det
end
-- Return this object
return obj
end
end,
-- Applies matrix to point
transform = function(x, y, z, w)
-- Check arguments
if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" or (w ~= nil and type(w) ~= "number") then
error("point (3 or 4 numbers) expected", 2)
end
-- Set 4th coordinate
if not w then
w = 1
end
-- Calculate new point
return x * matrix[1] + y * matrix[5] + z * matrix[9] + w * matrix[13],
x * matrix[2] + y * matrix[6] + z * matrix[10] + w * matrix[14],
x * matrix[3] + y * matrix[7] + z * matrix[11] + w * matrix[15],
x * matrix[4] + y * matrix[8] + z * matrix[12] + w * matrix[16]
end
}
return obj
end,
-- Degree between two 3d vectors
degree = function(x1, y1, z1, x2, y2, z2)
-- Check arguments
if type(x1) ~= "number" or type(y1) ~= "number" or type(z1) ~= "number" or
type(x2) ~= "number" or type(y2) ~= "number" or type(z2) ~= "number" then
error("2 vectors (as 6 numbers) expected", 2)
end
-- Calculate degree
local degree = math.deg(
math.acos(
(x1 * x2 + y1 * y2 + z1 * z2) /
(Yutils.math.distance(x1, y1, z1) * Yutils.math.distance(x2, y2, z2))
)
)
-- Return with sign by clockwise direction
return (x1*y2 - y1*x2) < 0 and -degree or degree
end,
-- Length of vector
distance = function(x, y, z)
-- Check arguments
if type(x) ~= "number" or type(y) ~= "number" or z ~= nil and type(z) ~= "number" then
error("one vector (2 or 3 numbers) expected", 2)
end
-- Calculate length
return z and math.sqrt(x*x + y*y + z*z) or math.sqrt(x*x + y*y)
end,
line_intersect = function(x0, y0, x1, y1, x2, y2, x3, y3, strict)
-- Check arguments
if type(x0) ~= "number" or type(y0) ~= "number" or type(x1) ~= "number" or type(y1) ~= "number" or
type(x2) ~= "number" or type(y2) ~= "number" or type(x3) ~= "number" or type(y3) ~= "number" or
strict ~= nil and type(strict) ~= "boolean" then
error("two lines and optional strictness flag expected", 2)
end
-- Get line vectors & check valid lengths
local x10, y10, x32, y32 = x0 - x1, y0 - y1, x2 - x3, y2 - y3
if x10 == 0 and y10 == 0 or x32 == 0 and y32 == 0 then
error("lines mustn't have zero length", 2)
end
-- Calculate determinant and check for parallel lines
local det = x10 * y32 - y10 * x32
if det ~= 0 then
-- Calculate line intersection (endless line lengths)
local pre, post = (x0 * y1 - y0 * x1), (x2 * y3 - y2 * x3)
local ix, iy = (pre * x32 - x10 * post) / det, (pre * y32 - y10 * post) / det
-- Check for line intersection with given line lengths
if strict then
local s, t = x10 ~= 0 and (ix - x1) / x10 or (iy - y1) / y10, x32 ~= 0 and (ix - x3) / x32 or (iy - y3) / y32
if s < 0 or s > 1 or t < 0 or t > 1 then
return 1/0 -- inf
end
end
-- Return intersection point
return ix, iy
end
end,
-- Get orthogonal vector of 2 given vectors
ortho = function(x1, y1, z1, x2, y2, z2)
-- Check arguments
if type(x1) ~= "number" or type(y1) ~= "number" or type(z1) ~= "number" or
type(x2) ~= "number" or type(y2) ~= "number" or type(z2) ~= "number" then
error("2 vectors (as 6 numbers) expected", 2)
end
-- Calculate orthogonal
return y1 * z2 - z1 * y2,
z1 * x2 - x1 * z2,
x1 * y2 - y1 * x2
end,
-- Generates a random number in given range with specific item distance
randomsteps = function(min, max, step)
-- Check arguments
if type(min) ~= "number" or type(max) ~= "number" or type(step) ~= "number" or max < min or step <= 0 then
error("minimal, maximal and step number expected", 2)
end
-- Generate random number
return math.min(min + math.random(0, math.ceil((max - min) / step)) * step, max)
end,
-- Rounds number
round = function(x, dec)
-- Check argument
if type(x) ~= "number" or dec ~= nil and type(dec) ~= "number" then
error("number and optional number expected", 2)
end
-- Return number rounded to wished decimal size
if dec and dec >= 1 then
dec = 10^math.floor(dec)
return math.floor(x * dec + 0.5) / dec
else
return math.floor(x + 0.5)
end
end,
-- Scales vector to given length
stretch = function(x, y, z, length)
-- Check arguments
if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" or type(length) ~= "number" then
error("vector (3d) and length expected", 2)
end
-- Get current vector length
local cur_length = Yutils.math.distance(x, y, z)
-- Scale vector to new length
if cur_length == 0 then
return 0, 0, 0
else
local factor = length / cur_length
return x * factor, y * factor, z * factor
end
end,
-- Trim number in range
trim = function(x, min, max)
-- Check arguments
if type(x) ~= "number" or type(min) ~= "number" or type(max) ~= "number" then
error("3 numbers expected", 2)
end
-- Limit number bigger-equal minimal value and smaller-equal maximal value
return x < min and min or x > max and max or x
end
},
-- Algorithm sublibrary
algorithm = {
-- Creates iterator through frame times
frames = function(starts, ends, dur)
-- Check arguments
if type(starts) ~= "number" or type(ends) ~= "number" or type(dur) ~= "number" or dur == 0 then
error("start, end and duration number expected", 2)
end
-- Iteration state
local i, n = 0, math.ceil((ends - starts) / dur)
-- Return iterator
return function()
i = i + 1
if i <= n then
local ret_starts = starts + (i-1) * dur
local ret_ends = ret_starts + dur
if dur < 0 and ret_ends < ends or dur > 0 and ret_ends > ends then
ret_ends = ends
end
return ret_starts, ret_ends, i, n
end
end
end,
-- Creates iterator through text lines
lines = function(text)
-- Check argument
if type(text) ~= "string" then
error("string expected", 2)
end
-- Return iterator
return function()
-- Still text left?
if text then
-- Find possible line endings
local cr = text:find("\r", 1, true)
local lf = text:find("\n", 1, true)
-- Find earliest line ending
local text_end, next_step = #text, 0
if lf then
text_end, next_step = lf-1, 2
end
if cr then
if not lf or cr < lf-1 then
text_end, next_step = cr-1, 2
elseif cr == lf-1 then
text_end, next_step = cr-1, 3
end
end
-- Cut line out & update text
local line = text:sub(1, text_end)
if next_step == 0 then
text = nil
else
text = text:sub(text_end+next_step)
end
-- Return current line
return line
end
end
end
},
-- Shape sublibrary
shape = {
-- Calculates shape bounding box
bounding = function(shape)
-- Check argument
if type(shape) ~= "string" then
error("shape expected", 2)
end
-- Bounding data
local x0, y0, x1, y1
-- Calculate minimal and maximal coordinates
Yutils.shape.filter(shape, function(x, y)
if x0 then
x0, y0, x1, y1 = math.min(x0, x), math.min(y0, y), math.max(x1, x), math.max(y1, y)
else
x0, y0, x1, y1 = x, y, x, y
end
end)
return x0, y0, x1, y1
end,
-- Extracts shapes by similar data in 2d data map
detect = function(width, height, data, compare_func)
-- Check arguments
if type(width) ~= "number" or math.floor(width) ~= width or width < 1 or type(height) ~= "number" or math.floor(height) ~= height or height < 1 or type(data) ~= "table" or #data < width * height or (compare_func ~= nil and type(compare_func) ~= "function") then
error("width, height, data and optional data compare function expected", 2)
end
-- Set default comparator
if not compare_func then
compare_func = function(a, b) return a == b end
end
-- Maximal data number to be processed
local data_n = width * height
-- Collect unique data elements
local elements = {n = 1, {value = data[1]}}
for i=2, data_n do
for j=1, elements.n do
if compare_func(data[i], elements[j].value) then
goto trace_element_found
end
end
elements.n = elements.n + 1
elements[elements.n] = {value = type(data[i]) == "table" and Yutils.table.copy(data[i]) or data[i]}
::trace_element_found::
end
-- Detection helper functions
local function index_to_x(i)
return (i-1) % width
end
local function index_to_y(i)
return math.floor((i-1) / width)
end
local function coord_to_index(x, y)
return 1 + x + y * width
end
local function find_direction(bitmap, x, y, last_direction)
local top_left, top_right, bottom_left, bottom_right =
x-1 >= 0 and y-1 >= 0 and bitmap[coord_to_index(x-1,y-1)] == 1 or false,
x < width and y-1 >= 0 and bitmap[coord_to_index(x,y-1)] == 1 or false,
x-1 >= 0 and y < height and bitmap[coord_to_index(x-1,y)] == 1 or false,
x < width and y < height and bitmap[coord_to_index(x,y)] == 1 or false
return last_direction == 8 and (
bottom_left and (
top_left and top_right and 6 or
top_left and 8 or
4
) or ( -- bottom_right
top_left and top_right and 4 or
top_right and 8 or
6
)
) or last_direction == 6 and (
top_left and (
top_right and bottom_right and 2 or
top_right and 6 or
8
)or ( -- bottom_left
top_right and bottom_right and 8 or
bottom_right and 6 or
2
)
) or last_direction == 2 and (
top_left and (
bottom_left and bottom_right and 6 or
bottom_left and 2 or
4
) or ( -- top_right
bottom_left and bottom_right and 4 or
bottom_right and 2 or
6
)
) or last_direction == 4 and (
top_right and (
top_left and bottom_left and 2 or
top_left and 4 or
8
) or ( -- bottom_right
top_left and bottom_left and 8 or
bottom_left and 4 or
2
)
)
end
local function extract_contour(bitmap, x, y, cw)
local contour, direction = {n = 1, cw and {x1 = x, y1 = y+1, x2 = x, y2 = y, direction = 8} or {x1 = x, y1 = y, x2 = x, y2 = y+1, direction = 2}}
repeat
x, y = contour[contour.n].x2, contour[contour.n].y2
direction = find_direction(bitmap, x, y, contour[contour.n].direction)
contour.n = contour.n + 1
contour[contour.n] = {x1 = x, y1 = y, x2 = direction == 4 and x-1 or direction == 6 and x+1 or x, y2 = direction == 8 and y-1 or direction == 2 and y+1 or y, direction = direction}
until contour[contour.n].x2 == contour[1].x1 and contour[contour.n].y2 == contour[1].y1
return contour
end
local function contour_indices(contour)
-- Get top & bottom line of contour
local min_y, max_y, line
for i=1, contour.n do
line = contour[i]
if line.direction == 8 then
min_y, max_y = min_y and math.min(min_y, line.y2) or line.y2, max_y and math.max(max_y, line.y2) or line.y2
elseif line.direction == 2 then
min_y, max_y = min_y and math.min(min_y, line.y1) or line.y1, max_y and math.max(max_y, line.y1) or line.y1
end
end
-- Get indices by scanlines
local indices, h_stops, h_stops_n, j = {n = 0}
for y=min_y, max_y do
h_stops, h_stops_n = {}, 0
for i=1, contour.n do
line = contour[i]
if line.direction == 8 and line.y2 == y or line.direction == 2 and line.y1 == y then
h_stops_n = h_stops_n + 1
h_stops[h_stops_n] = line.x1
end
end
table.sort(h_stops)
for i=1, h_stops_n, 2 do
j = coord_to_index(h_stops[i], y)
for x_off=0, h_stops[i+1] - h_stops[i] - 1 do
indices.n = indices.n + 1
indices[indices.n] = j + x_off
end
end
end
return indices
end
local function merge_contour_lines(contour)
local i = 1
while i < contour.n do
if contour[i].direction == contour[i+1].direction then
contour[i].x2, contour[i].y2 = contour[i+1].x2, contour[i+1].y2
table.remove(contour, i+1)
contour.n = contour.n - 1
else
i = i + 1
end
end
if contour.n > 1 and contour[1].direction == contour[contour.n].direction then
contour[1].x1, contour[1].y1 = contour[contour.n].x1, contour[contour.n].y1
table.remove(contour)
contour.n = contour.n - 1
end
return contour
end
local function contour_to_shape(contour)
local shape, shape_n, line = {string.format("m %d %d l", contour[1].x1, contour[1].y1)}, 1
for i=1, contour.n do
line = contour[i]
shape_n = shape_n + 1
shape[shape_n] = string.format("%d %d", line.x2, line.y2)
end
return table.concat(shape, " ")
end
-- Find shapes for elements
local element, element_shapes, shape, shape_n, element_contour, element_hole_contour, indices, hole_indices
local bitmap = {}
for i=1, elements.n do
element, element_shapes = elements[i].value, {n = 0}
-- Create bitmap of data for current element
for i=1, data_n do
bitmap[i] = compare_func(data[i], element) and 1 or 0
end
-- Find first upper-left element of shapes
for i=1, data_n do
if bitmap[i] == 1 then
-- Detect contour
element_contour = extract_contour(bitmap, index_to_x(i), index_to_y(i), true)
indices = contour_indices(element_contour)
shape, shape_n = {contour_to_shape(merge_contour_lines(element_contour))}, 1
-- Detect contour holes
for i=1, indices.n do
i = indices[i]
if bitmap[i] == 0 then
element_hole_contour = extract_contour(bitmap, index_to_x(i), index_to_y(i), false)
hole_indices = contour_indices(element_hole_contour)
shape_n = shape_n + 1
shape[shape_n] = contour_to_shape(merge_contour_lines(element_hole_contour))
for i=1, hole_indices.n do
i = hole_indices[i]
bitmap[i] = bitmap[i] + 1
end
end
end
-- Remove contour from bitmap
for i=1, indices.n do
i = indices[i]
bitmap[i] = bitmap[i] - 1
end
-- Add shape to element
element_shapes.n = element_shapes.n + 1
element_shapes[element_shapes.n] = table.concat(shape, " ")
end
end
-- Set shapes to element
elements[i].shapes = element_shapes
end
-- Return shapes by element
return elements
end,
-- Filters shape points
filter = function(shape, filter)
-- Check arguments
if type(shape) ~= "string" or type(filter) ~= "function" then
error("shape and filter function expected", 2)
end
-- Iterate through space separated tokens
local token_start, token_end, token, token_num = 1
local point_start, typ, x, new_point
repeat
token_start, token_end, token = shape:find("([^%s]+)", token_start)
if token_start then
-- Continue by token type / is number
token_num = tonumber(token)
if not token_num then
-- Set point type
point_start, typ, x = token_start, token
else
-- Set point coordinate
if not x then
-- Set x coordinate
if not point_start then
point_start = token_start
end
x = token_num
else
-- Apply filter on completed point
x, token_num = filter(x, token_num, typ)
-- Point to replace?
if type(x) == "number" and type(token_num) == "number" then
new_point = typ and string.format("%s %s %s", typ, Yutils.math.round(x, FP_PRECISION), Yutils.math.round(token_num, FP_PRECISION)) or
string.format("%s %s", Yutils.math.round(x, FP_PRECISION), Yutils.math.round(token_num, FP_PRECISION))
shape = string.format("%s%s%s", shape:sub(1, point_start-1), new_point, shape:sub(token_end+1))
token_end = point_start + #new_point - 1
end
-- Reset point / prepare next one
point_start, typ, x = nil
end
end
-- Increase shape start position to next possible token
token_start = token_end + 1
end
until not token_start
-- Return (modified) shape
return shape
end,
-- Converts shape curves to lines
flatten = function(shape)
-- Check argument
if type(shape) ~= "string" then
error("shape expected", 2)
end
-- 4th degree curve subdivider
local function curve4_subdivide(x0, y0, x1, y1, x2, y2, x3, y3, pct)
-- Calculate points on curve vectors
local x01, y01, x12, y12, x23, y23 = (x0+x1)*pct, (y0+y1)*pct, (x1+x2)*pct, (y1+y2)*pct, (x2+x3)*pct, (y2+y3)*pct
local x012, y012, x123, y123 = (x01+x12)*pct, (y01+y12)*pct, (x12+x23)*pct, (y12+y23)*pct
local x0123, y0123 = (x012+x123)*pct, (y012+y123)*pct
-- Return new 2 curves
return x0, y0, x01, y01, x012, y012, x0123, y0123,
x0123, y0123, x123, y123, x23, y23, x3, y3
end
-- Check flatness of 4th degree curve with angles
local function curve4_is_flat(x0, y0, x1, y1, x2, y2, x3, y3, tolerance)
-- Pack curve vectors
local vecs = {{x1 - x0, y1 - y0}, {x2 - x1, y2 - y1}, {x3 - x2, y3 - y2}}
-- Remove zero length vectors
local i, n = 1, #vecs
while i <= n do
if vecs[i][1] == 0 and vecs[i][2] == 0 then
table.remove(vecs, i)
n = n - 1
else
i = i + 1
end
end
-- Check flatness on remaining vectors
for i=2, n do
if math.abs(Yutils.math.degree(vecs[i-1][1], vecs[i-1][2], 0, vecs[i][1], vecs[i][2], 0)) > tolerance then
return false
end
end
return true
end
-- Convert 4th degree curve to line points
local function curve4_to_lines(x0, y0, x1, y1, x2, y2, x3, y3)
-- Line points buffer
local pts, pts_n = {x0, y0}, 2
-- Conversion in recursive processing
local function convert_recursive(x0, y0, x1, y1, x2, y2, x3, y3)
if curve4_is_flat(x0, y0, x1, y1, x2, y2, x3, y3, CURVE_TOLERANCE) then
pts[pts_n+1] = x3
pts[pts_n+2] = y3
pts_n = pts_n + 2
else
local x10, y10, x11, y11, x12, y12, x13, y13, x20, y20, x21, y21, x22, y22, x23, y23 = curve4_subdivide(x0, y0, x1, y1, x2, y2, x3, y3, 0.5)
convert_recursive(x10, y10, x11, y11, x12, y12, x13, y13)
convert_recursive(x20, y20, x21, y21, x22, y22, x23, y23)
end
end
convert_recursive(x0, y0, x1, y1, x2, y2, x3, y3)
-- Return resulting points
return pts
end
-- Search for curves
local curves_start, curves_end, x0, y0 = 1
local curve_start, curve_end, x1, y1, x2, y2, x3, y3
local line_points, line_curve
repeat
curves_start, curves_end, x0, y0 = shape:find("([^%s]+)%s+([^%s]+)%s+b%s+", curves_start)
x0, y0 = tonumber(x0), tonumber(y0)
-- Curve(s) found!
if x0 and y0 then
-- Replace curves type by lines type
shape = shape:sub(1, curves_start-1) .. shape:sub(curves_start):gsub("b", "l", 1)
-- Search for single curves
curve_start = curves_end + 1
repeat
curve_start, curve_end, x1, y1, x2, y2, x3, y3 = shape:find("([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+([^%s]+)", curve_start)
x1, y1, x2, y2, x3, y3 = tonumber(x1), tonumber(y1), tonumber(x2), tonumber(y2), tonumber(x3), tonumber(y3)
if x1 and y1 and x2 and y2 and x3 and y3 then
-- Convert curve to lines
local line_points = curve4_to_lines(x0, y0, x1, y1, x2, y2, x3, y3)
for i=1, #line_points do
line_points[i] = Yutils.math.round(line_points[i], FP_PRECISION)
end
line_curve = table.concat(line_points, " ")
shape = string.format("%s%s%s", shape:sub(1, curve_start-1), line_curve, shape:sub(curve_end+1))
curve_end = curve_start + #line_curve - 1
-- Set next start point to current last point
x0, y0 = x3, y3
-- Increase search start position to next possible curve
curve_start = curve_end + 1
end
until not (x1 and y1 and x2 and y2 and x3 and y3)
-- Increase search start position to next possible curves
curves_start = curves_end + 1
end
until not (x0 and y0)
-- Return shape without curves
return shape
end,
-- Projects shape on shape
glue = function(src_shape, dst_shape, transform_callback)
-- Check arguments
if type(src_shape) ~= "string" or type(dst_shape) ~= "string" or (transform_callback ~= nil and type(transform_callback) ~= "function") then
error("2 shapes and optional callback function expected", 2)
end
-- Trim destination shape to first figure
local _, figure_end = dst_shape:find("^%s*m.-m")
if figure_end then
dst_shape = dst_shape:sub(1, figure_end - 1)
end
-- Collect destination shape/figure lines + lengths
local dst_lines, dst_lines_n = {}, 0
local dst_lines_length, dst_line, last_point = 0
Yutils.shape.filter(Yutils.shape.flatten(dst_shape), function(x, y)
if last_point then
dst_line = {last_point[1], last_point[2], x - last_point[1], y - last_point[2], Yutils.math.distance(x - last_point[1], y - last_point[2])}
if dst_line[5] > 0 then
dst_lines_n = dst_lines_n + 1
dst_lines[dst_lines_n] = dst_line
dst_lines_length = dst_lines_length + dst_line[5]
end
end
last_point = {x, y}
end)
-- Any destination line?
if dst_lines_n > 0 then
-- Add relative positions to destination lines
local cur_length = 0
for _, dst_line in ipairs(dst_lines) do
dst_line[6] = cur_length / dst_lines_length
cur_length = cur_length + dst_line[5]
dst_line[7] = cur_length / dst_lines_length
end
-- Get source shape exact bounding box
local x0, _, x1, y1 = Yutils.shape.bounding(Yutils.shape.flatten(src_shape))
-- Source shape has body?
if x0 and x1 > x0 then
-- Source shape width
local w = x1 - x0
-- Shift source shape on destination shape
local x_pct, y_off, x_pct_temp, y_off_temp
local dst_line_pos, ovec_x, ovec_y
return Yutils.shape.filter(src_shape, function(x, y)
-- Get relative source point to baseline
x_pct, y_off = (x - x0) / w, y - y1
if transform_callback then
x_pct_temp, y_off_temp = transform_callback(x_pct, y_off)
if type(x_pct_temp) == "number" and type(y_off_temp) == "number" then
x_pct, y_off = math.max(0, math.min(x_pct_temp, 1)), y_off_temp
end
end
-- Search for destination point, relative to source point
for i=1, dst_lines_n do
dst_line = dst_lines[i]
if x_pct >= dst_line[6] and x_pct <= dst_line[7] then
dst_line_pos = (x_pct - dst_line[6]) / (dst_line[7] - dst_line[6])
-- Span orthogonal vector to baseline for final source to destination projection
ovec_x, ovec_y = Yutils.math.ortho(dst_line[3], dst_line[4], 0, 0, 0, -1)
ovec_x, ovec_y = Yutils.math.stretch(ovec_x, ovec_y, 0, y_off)
return dst_line[1] + dst_line_pos * dst_line[3] + ovec_x,
dst_line[2] + dst_line_pos * dst_line[4] + ovec_y
end
end
end)
end
end
end,
-- Shifts shape coordinates
move = function(shape, x, y)
-- Check arguments
if type(shape) ~= "string" or type(x) ~= "number" or type(y) ~= "number" then
error("shape, horizontal shift and vertical shift expected", 2)
end
-- Shift!
return Yutils.shape.filter(shape, function(cx, cy)
return cx + x, cy + y
end)
end,
-- Splits shape lines into shorter segments
split = function(shape, max_len)
-- Check arguments
if type(shape) ~= "string" or type(max_len) ~= "number" or max_len <= 0 then
error("shape and maximal line length expected", 2)
end
-- Remove shape closings (figures become line-completed)
shape = shape:gsub("%s+c", "")
-- Line splitter + string encoder
local function line_split(x0, y0, x1, y1)
-- Line direction & length
local rel_x, rel_y = x1 - x0, y1 - y0
local distance = Yutils.math.distance(rel_x, rel_y)
-- Line too long -> split!
if distance > max_len then
-- Generate line segments
local lines, lines_n, distance_rest, pct = {}, 0, distance % max_len
for cur_distance = distance_rest > 0 and distance_rest or max_len, distance, max_len do
pct = cur_distance / distance
lines_n = lines_n + 1
lines[lines_n] = string.format("%s %s", Yutils.math.round(x0 + rel_x * pct, FP_PRECISION), Yutils.math.round(y0 + rel_y * pct, FP_PRECISION))
end
return table.concat(lines, " ")
-- No line split
else
return string.format("%s %s", Yutils.math.round(x1, FP_PRECISION), Yutils.math.round(y1, FP_PRECISION))
end
end
-- Build new shape with shorter lines
local new_shape, new_shape_n = {}, 0
local line_mode, last_point, last_move
Yutils.shape.filter(shape, function(x, y, typ)
-- Close last figure of new shape
if typ == "m" and last_move and not (last_point[1] == last_move[1] and last_point[2] == last_move[2]) then
if not line_mode then
new_shape_n = new_shape_n + 1
new_shape[new_shape_n] = "l"
end
new_shape_n = new_shape_n + 1
new_shape[new_shape_n] = line_split(last_point[1], last_point[2], last_move[1], last_move[2])
end
-- Add current type to new shape
if typ then
new_shape_n = new_shape_n + 1
new_shape[new_shape_n] = typ
end
-- En-/disable line mode by current type
if typ then
line_mode = typ == "l"
end
-- Add current point or splitted line to new shape
new_shape_n = new_shape_n + 1
new_shape[new_shape_n] = line_mode and last_point and line_split(last_point[1], last_point[2], x, y) or string.format("%s %s", Yutils.math.round(x, FP_PRECISION), Yutils.math.round(y, FP_PRECISION))
-- Update last point & move
last_point = {x, y}
if typ == "m" then
last_move = {x, y}
end
end)
-- Close last figure of new shape
if last_move and not (last_point[1] == last_move[1] and last_point[2] == last_move[2]) then
if not line_mode then
new_shape_n = new_shape_n + 1
new_shape[new_shape_n] = "l"
end
new_shape_n = new_shape_n + 1
new_shape[new_shape_n] = line_split(last_point[1], last_point[2], last_move[1], last_move[2])
end
return table.concat(new_shape, " ")
end,
-- Converts shape to stroke version
to_outline = function(shape, width_xy, width_y, mode)
-- Check arguments
if type(shape) ~= "string" or type(width_xy) ~= "number" or width_y ~= nil and type(width_y) ~= "number" or mode ~= nil and type(mode) ~= "string" then
error("shape, line width (general or horizontal and vertical) and optional mode expected", 2)
elseif width_y and (width_xy < 0 or width_y < 0 or not (width_xy > 0 or width_y > 0)) or width_xy <= 0 then
error("one width must be >0", 2)
elseif mode and mode ~= "round" and mode ~= "bevel" and mode ~= "miter" then
error("valid mode expected", 2)
end
-- Line width values
local width, xscale, yscale
if width_y and width_xy ~= width_y then
width = math.max(width_xy, width_y)
xscale, yscale = width_xy / width, width_y / width
else
width, xscale, yscale = width_xy, 1, 1
end
-- Collect figures
local figures, figures_n, figure, figure_n = {}, 0, {}, 0
local last_move
Yutils.shape.filter(shape, function(x, y, typ)
-- Check point type
if typ and not (typ == "m" or typ == "l") then
error("shape have to contain only \"moves\" and \"lines\"", 2)
end
-- New figure?
if not last_move or typ == "m" then
-- Enough points in figure?
if figure_n > 2 then
-- Last point equal to first point? (yes: remove him)
if last_move and figure[figure_n][1] == last_move[1] and figure[figure_n][2] == last_move[2] then
figure[figure_n] = nil
end
-- Save figure
figures_n = figures_n + 1
figures[figures_n] = figure
end
-- Clear figure for new one
figure, figure_n = {}, 0
-- Save last move for figure closing check
last_move = {x, y}
end
-- Add point to current figure (if not copy of last)
if figure_n == 0 or not(figure[figure_n][1] == x and figure[figure_n][2] == y) then
figure_n = figure_n + 1
figure[figure_n] = {x, y}
end
end)
-- Insert last figure (with enough points)
if figure_n > 2 then
-- Last point equal to first point? (yes: remove him)
if last_move and figure[figure_n][1] == last_move[1] and figure[figure_n][2] == last_move[2] then
figure[figure_n] = nil
end
-- Save figure
figures_n = figures_n + 1
figures[figures_n] = figure
end
-- Create stroke shape out of figures
local stroke_shape, stroke_shape_n = {}, 0
for fi, figure in ipairs(figures) do
-- One pass for inner, one for outer outline
for i = 1, 2 do
-- Outline buffer
local outline, outline_n = {}, 0
-- Point iteration order = inner or outer outline
local loop_begin, loop_end, loop_steps
if i == 1 then
loop_begin, loop_end, loop_step = #figure, 1, -1
else
loop_begin, loop_end, loop_step = 1, #figure, 1
end
-- Iterate through figure points
for pi = loop_begin, loop_end, loop_step do
-- Collect current, previous and next point
local point = figure[pi]
local pre_point, post_point
if i == 1 then
if pi == 1 then
pre_point = figure[pi+1]
post_point = figure[#figure]
elseif pi == #figure then
pre_point = figure[1]
post_point = figure[pi-1]
else
pre_point = figure[pi+1]
post_point = figure[pi-1]
end
else
if pi == 1 then
pre_point = figure[#figure]
post_point = figure[pi+1]
elseif pi == #figure then
pre_point = figure[pi-1]
post_point = figure[1]
else
pre_point = figure[pi-1]
post_point = figure[pi+1]
end
end
-- Calculate orthogonal vectors to both neighbour points
local vec1_x, vec1_y, vec2_x, vec2_y = point[1]-pre_point[1], point[2]-pre_point[2], point[1]-post_point[1], point[2]-post_point[2]
local o_vec1_x, o_vec1_y = Yutils.math.ortho(vec1_x, vec1_y, 0, 0, 0, 1)
o_vec1_x, o_vec1_y = Yutils.math.stretch(o_vec1_x, o_vec1_y, 0, width)
local o_vec2_x, o_vec2_y = Yutils.math.ortho(vec2_x, vec2_y, 0, 0, 0, -1)
o_vec2_x, o_vec2_y = Yutils.math.stretch(o_vec2_x, o_vec2_y, 0, width)
-- Check for gap or edge join
local is_x, is_y = Yutils.math.line_intersect(point[1] + o_vec1_x - vec1_x, point[2] + o_vec1_y - vec1_y,
point[1] + o_vec1_x, point[2] + o_vec1_y,
point[1] + o_vec2_x - vec2_x, point[2] + o_vec2_y - vec2_y,
point[1] + o_vec2_x, point[2] + o_vec2_y,
true)
if is_y then
-- Add gap point
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s",
outline_n == 1 and "m " or outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + (is_x - point[1]) * xscale, FP_PRECISION), Yutils.math.round(point[2] + (is_y - point[2]) * yscale, FP_PRECISION))
else
-- Add first edge point
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s",
outline_n == 1 and "m " or outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + o_vec1_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + o_vec1_y * yscale, FP_PRECISION))
-- Create join by mode
if mode == "bevel" then
-- Nothing to add!
elseif mode == "miter" then
-- Add mid edge point(s)
is_x, is_y = Yutils.math.line_intersect(point[1] + o_vec1_x - vec1_x, point[2] + o_vec1_y - vec1_y,
point[1] + o_vec1_x, point[2] + o_vec1_y,
point[1] + o_vec2_x - vec2_x, point[2] + o_vec2_y - vec2_y,
point[1] + o_vec2_x, point[2] + o_vec2_y)
if is_y then -- Vectors intersect
local is_vec_x, is_vec_y = is_x - point[1], is_y - point[2]
local is_vec_len = Yutils.math.distance(is_vec_x, is_vec_y)
if is_vec_len > MITER_LIMIT then
local fix_scale = MITER_LIMIT / is_vec_len
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s %s %s",
outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + (o_vec1_x + (is_vec_x - o_vec1_x) * fix_scale) * xscale, FP_PRECISION), Yutils.math.round(point[2] + (o_vec1_y + (is_vec_y - o_vec1_y) * fix_scale) * yscale, FP_PRECISION),
Yutils.math.round(point[1] + (o_vec2_x + (is_vec_x - o_vec2_x) * fix_scale) * xscale, FP_PRECISION), Yutils.math.round(point[2] + (o_vec2_y + (is_vec_y - o_vec2_y) * fix_scale) * yscale, FP_PRECISION))
else
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s",
outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + is_vec_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + is_vec_y * yscale, FP_PRECISION))
end
else -- Parallel vectors
vec1_x, vec1_y = Yutils.math.stretch(vec1_x, vec1_y, 0, MITER_LIMIT)
vec2_x, vec2_y = Yutils.math.stretch(vec2_x, vec2_y, 0, MITER_LIMIT)
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s %s %s",
outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + (o_vec1_x + vec1_x) * xscale, FP_PRECISION), Yutils.math.round(point[2] + (o_vec1_y + vec1_y) * yscale, FP_PRECISION),
Yutils.math.round(point[1] + (o_vec2_x + vec2_x) * xscale, FP_PRECISION), Yutils.math.round(point[2] + (o_vec2_y + vec2_y) * yscale, FP_PRECISION))
end
else -- not mode or mode == "round"
-- Calculate degree & circumference between orthogonal vectors
local degree = Yutils.math.degree(o_vec1_x, o_vec1_y, 0, o_vec2_x, o_vec2_y, 0)
local circ = math.abs(math.rad(degree)) * width
-- Join needed?
if circ > MAX_CIRCUMFERENCE then
-- Add curve edge points
local circ_rest = circ % MAX_CIRCUMFERENCE
for cur_circ = circ_rest > 0 and circ_rest or MAX_CIRCUMFERENCE, circ - MAX_CIRCUMFERENCE, MAX_CIRCUMFERENCE do
local curve_vec_x, curve_vec_y = rotate2d(o_vec1_x, o_vec1_y, cur_circ / circ * degree)
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s",
outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + curve_vec_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + curve_vec_y * yscale, FP_PRECISION))
end
end
end
-- Add end edge point
outline_n = outline_n + 1
outline[outline_n] = string.format("%s%s %s",
outline_n == 2 and "l " or "",
Yutils.math.round(point[1] + o_vec2_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + o_vec2_y * yscale, FP_PRECISION))
end
end
-- Insert inner or outer outline to stroke shape
stroke_shape_n = stroke_shape_n + 1
stroke_shape[stroke_shape_n] = table.concat(outline, " ")
end
end
return table.concat(stroke_shape, " ")
end,
-- Converts shape to pixels
to_pixels = function(shape)
-- Check argument
if type(shape) ~= "string" then
error("shape expected", 2)
end
-- Scale values for supersampled rendering
local upscale = SUPERSAMPLING
local downscale = 1 / upscale
-- Upscale shape for later downsampling
shape = Yutils.shape.filter(shape, function(x, y)
return x * upscale, y * upscale
end)
-- Get shape bounding
local x1, y1, x2, y2 = Yutils.shape.bounding(shape)
if not y2 then
error("not enough shape points", 2)
end
-- Bring shape near origin in positive room
local shift_x, shift_y = -(x1 - x1 % upscale), -(y1 - y1 % upscale)
shape = Yutils.shape.move(shape, shift_x, shift_y)
-- Renderer (on binary image with aliasing)
local function render_shape(width, height, image, shape)
-- Collect lines (points + vectors)
local lines, lines_n, last_point, last_move = {}, 0
Yutils.shape.filter(Yutils.shape.flatten(shape), function(x, y, typ)
x, y = Yutils.math.round(x), Yutils.math.round(y) -- Use integers to avoid rounding errors
-- Move
if typ == "m" then
-- Close figure with non-horizontal line in image
if last_move and last_move[2] ~= last_point[2] and not (last_point[2] < 0 and last_move[2] < 0) and not (last_point[2] > height and last_move[2] > height) then
lines_n = lines_n + 1
lines[lines_n] = {last_point[1], last_point[2], last_move[1] - last_point[1], last_move[2] - last_point[2]}
end
last_move = {x, y}
-- Non-horizontal line in image
elseif last_point and last_point[2] ~= y and not (last_point[2] < 0 and y < 0) and not (last_point[2] > height and y > height) then
lines_n = lines_n + 1
lines[lines_n] = {last_point[1], last_point[2], x - last_point[1], y - last_point[2]}
end
-- Remember last point
last_point = {x, y}
end)
-- Close last figure with non-horizontal line in image
if last_move and last_move[2] ~= last_point[2] and not (last_point[2] < 0 and last_move[2] < 0) and not (last_point[2] > height and last_move[2] > height) then
lines_n = lines_n + 1
lines[lines_n] = {last_point[1], last_point[2], last_move[1] - last_point[1], last_move[2] - last_point[2]}
end
-- Calculates line x horizontal line intersection
local function line_x_hline(x, y, vx, vy, y2)
if vy ~= 0 then
local s = (y2 - y) / vy
if s >= 0 and s <= 1 then
return x + s * vx, y2
end
end
end
-- Scan image rows in shape
local _, y1, _, y2 = Yutils.shape.bounding(shape)
for y = math.max(math.floor(y1), 0), math.min(math.ceil(y2), height)-1 do
-- Collect row intersections with lines
local row_stops, row_stops_n = {}, 0
for i=1, lines_n do
local line = lines[i]
local cx = line_x_hline(line[1], line[2], line[3], line[4], y + 0.5)
if cx then
row_stops_n = row_stops_n + 1
row_stops[row_stops_n] = {Yutils.math.trim(cx, 0, width), line[4] > 0 and 1 or -1} -- image trimmed stop position & line vertical direction
end
end
-- Enough intersections / something to render?
if row_stops_n > 1 then
-- Sort row stops by horizontal position
table.sort(row_stops, function(a, b)
return a[1] < b[1]
end)
-- Render!
local status, row_index = 0, 1 + y * width
for i = 1, row_stops_n-1 do
status = status + row_stops[i][2]
if status ~= 0 then
for x=math.ceil(row_stops[i][1]-0.5), math.floor(row_stops[i+1][1]+0.5)-1 do
image[row_index + x] = true
end
end
end
end
end
end
-- Create image
local img_width, img_height, img_data = math.ceil((x2 + shift_x) * downscale) * upscale, math.ceil((y2 + shift_y) * downscale) * upscale, {}
for i=1, img_width*img_height do
img_data[i] = false
end
-- Render shape on image
render_shape(img_width, img_height, img_data, shape)
-- Extract pixels from image
local pixels, pixels_n, opacity = {}, 0
for y=0, img_height-upscale, upscale do
for x=0, img_width-upscale, upscale do
opacity = 0
for yy=0, upscale-1 do
for xx=0, upscale-1 do
if img_data[1 + (y+yy) * img_width + (x+xx)] then
opacity = opacity + 255
end
end
end
if opacity > 0 then
pixels_n = pixels_n + 1
pixels[pixels_n] = {
alpha = opacity * (downscale * downscale),
x = (x - shift_x) * downscale,
y = (y - shift_y) * downscale
}
end
end
end
return pixels
end,
-- Applies matrix to shape coordinates
transform = function(shape, matrix)
-- Check arguments
if type(shape) ~= "string" or type(matrix) ~= "table" or type(matrix.transform) ~= "function" then
error("shape and matrix expected", 2)
end
local success, x, y, z, w = pcall(matrix.transform, 1, 1, 1)
if not success or type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" or type(w) ~= "number" then
error("matrix transform method invalid", 2)
end
-- Filter shape with matrix
return Yutils.shape.filter(shape, function(x, y)
x, y, z, w = matrix.transform(x, y, 0)
return x / w, y / w
end)
end
},
-- Advanced substation alpha sublibrary
ass = {
-- Converts between milliseconds and ASS timestamp
convert_time = function(ass_ms)
-- Process by argument
if type(ass_ms) == "number" and ass_ms >= 0 then -- Milliseconds
return string.format("%d:%02d:%02d.%02d",
math.floor(ass_ms / 3600000) % 10,
math.floor(ass_ms % 3600000 / 60000),
math.floor(ass_ms % 60000 / 1000),
math.floor(ass_ms % 1000 / 10))
elseif type(ass_ms) == "string" and ass_ms:find("^%d:%d%d:%d%d%.%d%d$") then -- ASS timestamp
return ass_ms:sub(1,1) * 3600000 + ass_ms:sub(3,4) * 60000 + ass_ms:sub(6,7) * 1000 + ass_ms:sub(9,10) * 10
else
error("milliseconds or ASS timestamp expected", 2)
end
end,
-- Converts between color &/+ alpha numeric and ASS color &/+ alpha
convert_coloralpha = function(ass_r_a, g, b, a)
-- Process by argument(s)
if type(ass_r_a) == "number" and ass_r_a >= 0 and ass_r_a <= 255 then -- Alpha / red numeric
if type(g) == "number" and g >= 0 and g <= 255 and type(b) == "number" and b >= 0 and b <= 255 then -- Green + blue numeric
if type(a) == "number" and a >= 0 and a <= 255 then -- Alpha numeric
return string.format("&H%02X%02X%02X%02X", 255 - a, b, g, ass_r_a)
else
return string.format("&H%02X%02X%02X&", b, g, ass_r_a)
end
else
return string.format("&H%02X&", 255 - ass_r_a)
end
elseif type(ass_r_a) == "string" then -- ASS value
if ass_r_a:find("^&H%x%x&$") then -- ASS alpha
return 255 - tonumber(ass_r_a:sub(3,4), 16)
elseif ass_r_a:find("^&H%x%x%x%x%x%x&$") then -- ASS color
return tonumber(ass_r_a:sub(7,8), 16), tonumber(ass_r_a:sub(5,6), 16), tonumber(ass_r_a:sub(3,4), 16)
elseif ass_r_a:find("^&H%x%x%x%x%x%x%x%x$") then -- ASS color+alpha (style)
return tonumber(ass_r_a:sub(9,10), 16), tonumber(ass_r_a:sub(7,8), 16), tonumber(ass_r_a:sub(5,6), 16), 255 - tonumber(ass_r_a:sub(3,4), 16)
else
error("invalid string")
end
else
error("color, alpha or color+alpha as numeric or ASS expected", 2)
end
end,
-- Interpolates between two ASS colors &/+ alphas
interpolate_coloralpha = function(pct, ...)
-- Pack arguments
local args = {...}
args.n = #args
-- Check arguments
if type(pct) ~= "number" or pct < 0 or pct > 1 or args.n < 2 then
error("progress and at least two ASS values of same type (color, alpha or color+alpha) expected", 2)
end
for i=1, args.n do
if type(args[i]) ~= "string" then
error("ASS values must be strings", 2)
end
end
-- Pick first ASS value for interpolation
local i = math.min(1 + math.floor(pct * (args.n-1)), args.n-1)
-- Extract ASS value parts
local success1, ass_r_a1, g1, b1, a1 = pcall(Yutils.ass.convert_coloralpha, args[i])
local success2, ass_r_a2, g2, b2, a2 = pcall(Yutils.ass.convert_coloralpha, args[i+1])
if not success1 or not success2 then
error("invalid ASS value(s)", 2)
end
-- Process by ASS values type
local min_pct, max_pct = (i-1) / (args.n-1), i / (args.n-1)
local inner_pct = (pct - min_pct) / (max_pct - min_pct)
if a1 and a2 then -- Color + alpha
return Yutils.ass.convert_coloralpha(ass_r_a1 + (ass_r_a2 - ass_r_a1) * inner_pct, g1 + (g2 - g1) * inner_pct, b1 + (b2 - b1) * inner_pct, a1 + (a2 - a1) * inner_pct)
elseif b1 and not a1 and b2 and not a2 then -- Color
return Yutils.ass.convert_coloralpha(ass_r_a1 + (ass_r_a2 - ass_r_a1) * inner_pct, g1 + (g2 - g1) * inner_pct, b1 + (b2 - b1) * inner_pct)
elseif not g1 and not g2 then -- Alpha
return Yutils.ass.convert_coloralpha(ass_r_a1 + (ass_r_a2 - ass_r_a1) * inner_pct)
else
error("ASS values must be the same type", 2)
end
end,
-- Creates an ASS parser
create_parser = function(ass_text)
-- Check argument
if ass_text ~= nil and type(ass_text) ~= "string" then
error("optional string expected", 2)
end
-- Current section (for parsing validation)
local section = ""
-- ASS contents (just rendering relevant stuff)
local meta = {wrap_style = 0, scaled_border_and_shadow = true, play_res_x = 0, play_res_y = 0}
local styles = {}
local dialogs = {n = 0}
-- Create parser & getter object
local obj = {
parse_line = function(line)
-- Check argument
if type(line) ~= "string" then
error("string expected", 2)
end
-- Parse (by) section
if line:find("^%[.-%]$") then -- Define section
section = line:sub(2,-2)
return true
elseif section == "Script Info" then -- Meta
if line:find("^WrapStyle: %d$") then
meta.wrap_style = tonumber(line:sub(12))
return true
elseif line:find("^ScaledBorderAndShadow: %l+$") then
local value = line:sub(24)
if value == "yes" or value == "no" then
meta.scaled_border_and_shadow = value == "yes"
return true
end
elseif line:find("^PlayResX: %d+$") then
meta.play_res_x = tonumber(line:sub(11))
return true
elseif line:find("^PlayResY: %d+$") then
meta.play_res_y = tonumber(line:sub(11))
return true
end
elseif section == "V4+ Styles" then -- Styles
local name, fontname, fontsize, color1, color2, color3, color4,
bold, italic, underline, strikeout, scale_x, scale_y, spacing, angle, border_style,
outline, shadow, alignment, margin_l, margin_r, margin_v, encoding =
line:match("^Style: (.-),(.-),(%d+),(&H%x%x%x%x%x%x%x%x),(&H%x%x%x%x%x%x%x%x),(&H%x%x%x%x%x%x%x%x),(&H%x%x%x%x%x%x%x%x),(%-?[01]),(%-?[01]),(%-?[01]),(%-?[01]),(%d+%.?%d*),(%d+%.?%d*),(%-?%d+%.?%d*),(%-?%d+%.?%d*),([13]),(%d+%.?%d*),(%d+%.?%d*),([1-9]),(%d+%.?%d*),(%d+%.?%d*),(%d+%.?%d*),(%d+)$")
if encoding and tonumber(encoding) <= 255 then
local style = {
fontname = fontname,
fontsize = tonumber(fontsize),
bold = bold == "-1",
italic = italic == "-1",
underline = underline == "-1",
strikeout = strikeout == "-1",
scale_x = tonumber(scale_x),
scale_y = tonumber(scale_y),
spacing = tonumber(spacing),
angle = tonumber(angle),
border_style = border_style == "3",
outline = tonumber(outline),
shadow = tonumber(shadow),
alignment = tonumber(alignment),
margin_l = tonumber(margin_l),
margin_r = tonumber(margin_r),
margin_v = tonumber(margin_v),
encoding = tonumber(encoding)
}
local r, g, b, a = Yutils.ass.convert_coloralpha(color1)
style.color1 = Yutils.ass.convert_coloralpha(r, g, b)
style.alpha1 = Yutils.ass.convert_coloralpha(a)
r, g, b, a = Yutils.ass.convert_coloralpha(color2)
style.color2 = Yutils.ass.convert_coloralpha(r, g, b)
style.alpha2 = Yutils.ass.convert_coloralpha(a)
r, g, b, a = Yutils.ass.convert_coloralpha(color3)
style.color3 = Yutils.ass.convert_coloralpha(r, g, b)
style.alpha3 = Yutils.ass.convert_coloralpha(a)
r, g, b, a = Yutils.ass.convert_coloralpha(color4)
style.color4 = Yutils.ass.convert_coloralpha(r, g, b)
style.alpha4 = Yutils.ass.convert_coloralpha(a)
styles[name] = style
return true
end
elseif section == "Events" then -- Dialogs
local typ, layer, start_time, end_time, style, actor, margin_l, margin_r, margin_v, effect, text =
line:match("^(.-): (%d+),(%d:%d%d:%d%d%.%d%d),(%d:%d%d:%d%d%.%d%d),(.-),(.-),(%d+%.?%d*),(%d+%.?%d*),(%d+%.?%d*),(.-),(.*)$")
if text and (typ == "Dialogue" or typ == "Comment") then
dialogs.n = dialogs.n + 1
dialogs[dialogs.n] = {
comment = typ == "Comment",
layer = tonumber(layer),
start_time = Yutils.ass.convert_time(start_time),
end_time = Yutils.ass.convert_time(end_time),
style = style,
actor = actor,
margin_l = tonumber(margin_l),
margin_r = tonumber(margin_r),
margin_v = tonumber(margin_v),
effect = effect,
text = text
}
return true
end
end
-- Nothing parsed
return false
end,
meta = function()
return Yutils.table.copy(meta)
end,
styles = function()
return Yutils.table.copy(styles)
end,
dialogs = function(extended)
-- Check argument
if extended ~= nil and type(extended) ~= "boolean" then
error("optional extension flag expected")
end
-- Return extended dialogs
if extended then
-- Define text sizes getter
local function text_sizes(text, style)
local font = Yutils.decode.create_font(style.fontname, style.bold, style.italic, style.underline, style.strikeout, style.fontsize, style.scale_x/100, style.scale_y/100, style.spacing)
local extents, metrics = font.text_extents(text), font.metrics()
return extents.width, extents.height, metrics.ascent, metrics.descent, metrics.internal_leading, metrics.external_leading
end
if not pcall(text_sizes, "Test", {fontname="Arial",fontsize=10,bold=false,italic=false,underline=false,strikeout=false,scale_x=100,scale_y=100,spacing=0}) then -- Fonts aren't supported/available?
text_sizes = nil
end
-- Create dialogs copy & style storage
local dialogs, dialog_styles, dialog, style_dialogs = Yutils.table.copy(dialogs), {}
local space_width
-- Process single dialogs
for i=1, dialogs.n do
dialog = dialogs[i]
-- Append dialog to styles
style_dialogs = dialog_styles[dialog.style]
if not style_dialogs then
style_dialogs = {n = 0}
dialog_styles[dialog.style] = style_dialogs
end
style_dialogs.n = style_dialogs.n + 1
style_dialogs[style_dialogs.n] = dialog
-- Add dialog extra informations
dialog.i = i
dialog.duration = dialog.end_time - dialog.start_time
dialog.mid_time = dialog.start_time + dialog.duration / 2
dialog.styleref = styles[dialog.style]
dialog.text_stripped = dialog.text:gsub("{.-}", "")
-- Add dialog text sizes and positions (if possible)
if text_sizes and dialog.styleref then
dialog.width, dialog.height, dialog.ascent, dialog.descent, dialog.internal_leading, dialog.external_leading = text_sizes(dialog.text_stripped, dialog.styleref)
if meta.play_res_x > 0 and meta.play_res_y > 0 then
-- Horizontal position
if (dialog.styleref.alignment-1) % 3 == 0 then
dialog.left = dialog.margin_l ~= 0 and dialog.margin_l or dialog.styleref.margin_l
dialog.center = dialog.left + dialog.width / 2
dialog.right = dialog.left + dialog.width
dialog.x = dialog.left
elseif (dialog.styleref.alignment-2) % 3 == 0 then
dialog.left = meta.play_res_x / 2 - dialog.width / 2
dialog.center = dialog.left + dialog.width / 2
dialog.right = dialog.left + dialog.width
dialog.x = dialog.center
else
dialog.left = meta.play_res_x - (dialog.margin_r ~= 0 and dialog.margin_r or dialog.styleref.margin_r) - dialog.width
dialog.center = dialog.left + dialog.width / 2
dialog.right = dialog.left + dialog.width
dialog.x = dialog.right
end
-- Vertical position
if dialog.styleref.alignment > 6 then
dialog.top = dialog.margin_v ~= 0 and dialog.margin_v or dialog.styleref.margin_v
dialog.middle = dialog.top + dialog.height / 2
dialog.bottom = dialog.top + dialog.height
dialog.y = dialog.top
elseif dialog.styleref.alignment > 3 then
dialog.top = meta.play_res_y / 2 - dialog.height / 2
dialog.middle = dialog.top + dialog.height / 2
dialog.bottom = dialog.top + dialog.height
dialog.y = dialog.middle
else
dialog.top = meta.play_res_y - (dialog.margin_v ~= 0 and dialog.margin_v or dialog.styleref.margin_v) - dialog.height
dialog.middle = dialog.top + dialog.height / 2
dialog.bottom = dialog.top + dialog.height
dialog.y = dialog.bottom
end
end
space_width = text_sizes(" ", dialog.styleref)
end
-- Add dialog text chunks
dialog.text_chunked = {n = 0}
do
-- Has tags+text chunks?
local chunk_start, chunk_end = dialog.text:find("{.-}")
if not chunk_start then
dialog.text_chunked = {n = 1, {tags = "", text = dialog.text}}
else
-- First chunk without tags
if chunk_start ~= 1 then
dialog.text_chunked.n = dialog.text_chunked.n + 1
dialog.text_chunked[dialog.text_chunked.n] = {tags = "", text = dialog.text:sub(1, chunk_start-1)}
end
-- Chunks with tags
local chunk2_start, chunk2_end
repeat
chunk2_start, chunk2_end = dialog.text:find("{.-}", chunk_end+1)
dialog.text_chunked.n = dialog.text_chunked.n + 1
dialog.text_chunked[dialog.text_chunked.n] = {tags = dialog.text:sub(chunk_start+1, chunk_end-1), text = dialog.text:sub(chunk_end+1, chunk2_start and chunk2_start-1 or -1)}
chunk_start, chunk_end = chunk2_start, chunk2_end
until not chunk_start
end
end
-- Add dialog sylables
dialog.syls = {n = 0}
do
local last_time, text_chunk, pretags, kdur, posttags, syl = 0
-- Get sylables from text chunks
for i=1, dialog.text_chunked.n do
text_chunk = dialog.text_chunked[i]
pretags, kdur, posttags = text_chunk.tags:match("(.-)\\[kK][of]?(%d+)(.*)")
if posttags then -- All tag groups have to contain karaoke times or everything is invalid (=no sylables there)
syl = {
i = dialog.syls.n + 1,
start_time = last_time,
mid_time = last_time + kdur * 10 / 2,
end_time = last_time + kdur * 10,
duration = kdur * 10,
tags = pretags .. posttags
}
syl.prespace, syl.text, syl.postspace = text_chunk.text:match("(%s*)(%S*)(%s*)")
syl.prespace, syl.postspace = syl.prespace:len(), syl.postspace:len()
if text_sizes and dialog.styleref then
syl.width, syl.height, syl.ascent, syl.descent, syl.internal_leading, syl.external_leading = text_sizes(syl.text, dialog.styleref)
end
last_time = syl.end_time
dialog.syls.n = dialog.syls.n + 1
dialog.syls[dialog.syls.n] = syl
else
dialog.syls = {n = 0}
break
end
end
-- Calculate sylable positions with all sylables data already available
if dialog.syls.n > 0 and dialog.syls[1].width and meta.play_res_x > 0 and meta.play_res_y > 0 then
if dialog.styleref.alignment > 6 or dialog.styleref.alignment < 4 then
local cur_x = dialog.left
for i=1, dialog.syls.n do
syl = dialog.syls[i]
-- Horizontal position
cur_x = cur_x + syl.prespace * space_width
syl.left = cur_x
syl.center = syl.left + syl.width / 2
syl.right = syl.left + syl.width
syl.x = (dialog.styleref.alignment-1) % 3 == 0 and syl.left or
(dialog.styleref.alignment-2) % 3 == 0 and syl.center or
syl.right
cur_x = cur_x + syl.width + syl.postspace * space_width
-- Vertical position
syl.top = dialog.top
syl.middle = dialog.middle
syl.bottom = dialog.bottom
syl.y = dialog.y
end
else
local max_width, sum_height = 0, 0
for i=1, dialog.syls.n do
syl = dialog.syls[i]
max_width = math.max(max_width, syl.width)
sum_height = sum_height + syl.height
end
local cur_y, x_fix = meta.play_res_y / 2 - sum_height / 2
for i=1, dialog.syls.n do
syl = dialog.syls[i]
-- Horizontal position
x_fix = (max_width - syl.width) / 2
if dialog.styleref.alignment == 4 then
syl.left = dialog.left + x_fix
syl.center = syl.left + syl.width / 2
syl.right = syl.left + syl.width
syl.x = syl.left
elseif dialog.styleref.alignment == 5 then
syl.left = meta.play_res_x / 2 - syl.width / 2
syl.center = syl.left + syl.width / 2
syl.right = syl.left + syl.width
syl.x = syl.center
else -- dialog.styleref.alignment == 6
syl.left = dialog.right - syl.width - x_fix
syl.center = syl.left + syl.width / 2
syl.right = syl.left + syl.width
syl.x = syl.right
end
-- Vertical position
syl.top = cur_y
syl.middle = syl.top + syl.height / 2
syl.bottom = syl.top + syl.height
syl.y = syl.middle
cur_y = cur_y + syl.height
end
end
end
end
-- Add dialog words
dialog.words = {n = 0}
do
local word
for prespace, word_text, postspace in dialog.text_stripped:gmatch("(%s*)(%S+)(%s*)") do
word = {
i = dialog.words.n + 1,
start_time = dialog.start_time,
mid_time = dialog.mid_time,
end_time = dialog.end_time,
duration = dialog.duration,
text = word_text,
prespace = prespace:len(),
postspace = postspace:len()
}
if text_sizes and dialog.styleref then
word.width, word.height, word.ascent, word.descent, word.internal_leading, word.external_leading = text_sizes(word.text, dialog.styleref)
end
-- Add current word to dialog words
dialog.words.n = dialog.words.n + 1
dialog.words[dialog.words.n] = word
end
-- Calculate word positions with all words data already available
if dialog.words.n > 0 and dialog.words[1].width and meta.play_res_x > 0 and meta.play_res_y > 0 then
if dialog.styleref.alignment > 6 or dialog.styleref.alignment < 4 then
local cur_x = dialog.left
for i=1, dialog.words.n do
word = dialog.words[i]
-- Horizontal position
cur_x = cur_x + word.prespace * space_width
word.left = cur_x
word.center = word.left + word.width / 2
word.right = word.left + word.width
word.x = (dialog.styleref.alignment-1) % 3 == 0 and word.left or
(dialog.styleref.alignment-2) % 3 == 0 and word.center or
word.right
cur_x = cur_x + word.width + word.postspace * space_width
-- Vertical position
word.top = dialog.top
word.middle = dialog.middle
word.bottom = dialog.bottom
word.y = dialog.y
end
else
local max_width, sum_height = 0, 0
for i=1, dialog.words.n do
word = dialog.words[i]
max_width = math.max(max_width, word.width)
sum_height = sum_height + word.height
end
local cur_y, x_fix = meta.play_res_y / 2 - sum_height / 2
for i=1, dialog.words.n do
word = dialog.words[i]
-- Horizontal position
x_fix = (max_width - word.width) / 2
if dialog.styleref.alignment == 4 then
word.left = dialog.left + x_fix
word.center = word.left + word.width / 2
word.right = word.left + word.width
word.x = word.left
elseif dialog.styleref.alignment == 5 then
word.left = meta.play_res_x / 2 - word.width / 2
word.center = word.left + word.width / 2
word.right = word.left + word.width
word.x = word.center
else -- dialog.styleref.alignment == 6
word.left = dialog.right - word.width - x_fix
word.center = word.left + word.width / 2
word.right = word.left + word.width
word.x = word.right
end
-- Vertical position
word.top = cur_y
word.middle = word.top + word.height / 2
word.bottom = word.top + word.height
word.y = word.middle
cur_y = cur_y + word.height
end
end
end
end
-- Add dialog characters
dialog.chars = {n = 0}
do
local char, char_index, syl, word
for _, char_text in Yutils.utf8.chars(dialog.text_stripped) do
char = {
i = dialog.chars.n + 1,
start_time = dialog.start_time,
mid_time = dialog.mid_time,
end_time = dialog.end_time,
duration = dialog.duration,
text = char_text
}
char_index = 0
for i=1, dialog.syls.n do
syl = dialog.syls[i]
for _ in Yutils.utf8.chars(string.format("%s%s%s", string.rep(" ", syl.prespace), syl.text, string.rep(" ", syl.postspace))) do
char_index = char_index + 1
if char_index == char.i then
char.syl_i = syl.i
char.start_time = syl.start_time
char.mid_time = syl.mid_time
char.end_time = syl.end_time
char.duration = syl.duration
goto syl_reference_found
end
end
end
::syl_reference_found::
char_index = 0
for i=1, dialog.words.n do
word = dialog.words[i]
for _ in Yutils.utf8.chars(string.format("%s%s%s", string.rep(" ", word.prespace), word.text, string.rep(" ", word.postspace))) do
char_index = char_index + 1
if char_index == char.i then
char.word_i = word.i
goto word_reference_found
end
end
end
::word_reference_found::
if text_sizes and dialog.styleref then
char.width, char.height, char.ascent, char.descent, char.internal_leading, char.external_leading = text_sizes(char.text, dialog.styleref)
end
dialog.chars.n = dialog.chars.n + 1
dialog.chars[dialog.chars.n] = char
end
-- Calculate character positions with all characters data already available
if dialog.chars.n > 0 and dialog.chars[1].width and meta.play_res_x > 0 and meta.play_res_y > 0 then
if dialog.styleref.alignment > 6 or dialog.styleref.alignment < 4 then
local cur_x = dialog.left
for i=1, dialog.chars.n do
char = dialog.chars[i]
-- Horizontal position
char.left = cur_x
char.center = char.left + char.width / 2
char.right = char.left + char.width
char.x = (dialog.styleref.alignment-1) % 3 == 0 and char.left or
(dialog.styleref.alignment-2) % 3 == 0 and char.center or
char.right
cur_x = cur_x + char.width
-- Vertical position
char.top = dialog.top
char.middle = dialog.middle
char.bottom = dialog.bottom
char.y = dialog.y
end
else
local max_width, sum_height = 0, 0
for i=1, dialog.chars.n do
char = dialog.chars[i]
max_width = math.max(max_width, char.width)
sum_height = sum_height + char.height
end
local cur_y, x_fix = meta.play_res_y / 2 - sum_height / 2
for i=1, dialog.chars.n do
char = dialog.chars[i]
-- Horizontal position
x_fix = (max_width - char.width) / 2
if dialog.styleref.alignment == 4 then
char.left = dialog.left + x_fix
char.center = char.left + char.width / 2
char.right = char.left + char.width
char.x = char.left
elseif dialog.styleref.alignment == 5 then
char.left = meta.play_res_x / 2 - char.width / 2
char.center = char.left + char.width / 2
char.right = char.left + char.width
char.x = char.center
else -- dialog.styleref.alignment == 6
char.left = dialog.right - char.width - x_fix
char.center = char.left + char.width / 2
char.right = char.left + char.width
char.x = char.right
end
-- Vertical position
char.top = cur_y
char.middle = char.top + char.height / 2
char.bottom = char.top + char.height
char.y = char.middle
cur_y = cur_y + char.height
end
end
end
end
end
-- Add durations between dialogs
for _, dialogs in pairs(dialog_styles) do
table.sort(dialogs, function(dialog1, dialog2) return dialog1.start_time <= dialog2.start_time end)
for i=1, dialogs.n do
dialog = dialogs[i]
dialog.leadin = i == 1 and 1000.1 or dialog.start_time - dialogs[i-1].end_time
dialog.leadout = i == dialogs.n and 1000.1 or dialogs[i+1].start_time - dialog.end_time
end
end
-- Return modified copy
return dialogs
-- Return raw dialogs
else
return Yutils.table.copy(dialogs)
end
end
}
-- Parse ASS text
if ass_text then
for line in Yutils.algorithm.lines(ass_text) do
obj.parse_line(line) -- no errors possible
end
end
-- Return object
return obj
end
},
-- Decoder sublibrary
decode = {
-- Creates BMP file reader
create_bmp_reader = function(filename)
-- Check argument
if type(filename) ~= "string" then
error("bitmap filename expected", 2)
end
-- Image decoders
local function bmp_decode(filename)
-- Open file handle
local file = io.open(filename, "rb")
if file then
-- Read file header
local header = file:read(14)
if not header or #header ~= 14 then
return "couldn't read file header"
end
-- Check BMP signature
if header:sub(1,2) == "BM" then
-- Read relevant file header fields
local file_size, data_offset = bton(header:sub(3,6)), bton(header:sub(11,14))
-- Read DIB header
header = file:read(24)
if not header or #header ~= 24 then
return "couldn't read DIB header"
end
-- Read relevant DIB header fields
local width, height, planes, bit_depth, compression, data_size = bton(header:sub(5,8)), bton(header:sub(9,12)), bton(header:sub(13,14)), bton(header:sub(15,16)), bton(header:sub(17,20)), bton(header:sub(21,24))
-- Check read header data
if width >= 2^31 then
return "pixels in right-to-left order are not supported"
elseif planes ~= 1 then
return "planes must be 1"
elseif bit_depth ~= 24 and bit_depth ~= 32 then
return "bit depth must be 24 or 32"
elseif compression ~= 0 then
return "must be uncompressed RGB"
elseif data_size == 0 then
return "data size must not be zero"
end
-- Fix read header data
if height >= 2^31 then
height = height - 2^32
end
-- Read image data
file:seek("set", data_offset)
local data = file:read(data_size)
if not data or #data ~= data_size then
return "not enough data"
end
-- Calculate row size (round up to multiple of 4)
local row_size = math.floor((bit_depth * width + 31) / 32) * 4
-- All data read from file -> close handle (don't wait for GC)
file:close()
-- Return relevant bitmap informations
return file_size, width, height, bit_depth, data_size, data, row_size
end
end
end
local function png_decode(filename)
-- PNG decode library available?
if libpng then
-- Open file handle
local file = io.open(filename, "rb")
if file then
-- Load file content & close no further needed file handle
local file_content = file:read("*a")
file:close()
-- Get file size
local file_size = #file_content
-- Check PNG signature
if file_size > ffi.C.PNG_SIGNATURE_SIZE and libpng.png_sig_cmp(ffi.cast("png_const_bytep", file_content), 0, ffi.C.PNG_SIGNATURE_SIZE) == 0 then
-- Create PNG data structures & set error handlers
local ppng, pinfo, err = ffi.new("png_structp[1]"), ffi.new("png_infop[1]")
local function err_func(png, message)
libpng.png_destroy_read_struct(ppng, pinfo, nil)
err = ffi.string(message)
end
ppng[0] = libpng.png_create_read_struct(ffi.cast("char*", "1.5.14"), nil, err_func, err_func)
if not ppng[0] then
return "couldn't create png read structure"
end
pinfo[0] = libpng.png_create_info_struct(ppng[0])
if not pinfo[0] then
libpng.png_destroy_read_struct(ppng, nil, nil)
return "couldn't create png info structure"
end
-- Decode file content to png structures
local file_pos, file_content_bytes = 0, ffi.cast("png_bytep", file_content)
libpng.png_set_read_fn(ppng[0], nil, function(png, output_bytes, required_bytes)
if file_pos + required_bytes <= file_size then
ffi.C.memcpy(output_bytes, file_content_bytes+file_pos, required_bytes)
file_pos = file_pos + required_bytes
end
end)
libpng.png_read_png(ppng[0], pinfo[0], ffi.C.PNG_TRANSFORM_STRIP_16 + ffi.C.PNG_TRANSFORM_PACKING + ffi.C.PNG_TRANSFORM_EXPAND + ffi.C.PNG_TRANSFORM_BGR, nil)
if err then
return err
end
libpng.png_set_interlace_handling(ppng[0])
libpng.png_read_update_info(ppng[0], pinfo[0])
if err then
return err
end
-- Get header data
local width, height, color_type, row_size = libpng.png_get_image_width(ppng[0], pinfo[0]), libpng.png_get_image_height(ppng[0], pinfo[0]), libpng.png_get_color_type(ppng[0], pinfo[0]), libpng.png_get_rowbytes(ppng[0], pinfo[0])
local data_size, bit_depth = height * row_size
if color_type == ffi.C.PNG_COLOR_TYPE_RGB then
bit_depth = 24
elseif color_type == ffi.C.PNG_COLOR_TYPE_RGBA then
bit_depth = 32
else
libpng.png_destroy_read_struct(ppng, pinfo, nil)
return "png data conversion to BGR(A) colorspace failed"
end
-- Get image data
local rows = libpng.png_get_rows(ppng[0], pinfo[0])
local data, data_n = {}, 0
for i=0, height-1 do
data_n = data_n + 1
data[data_n] = ffi.string(rows[i], row_size)
end
data = table.concat(data)
-- Clean up
libpng.png_destroy_read_struct(ppng, pinfo, nil)
-- Return relevant bitmap informations
return file_size, width, height, bit_depth, data_size, data, row_size
end
end
end
end
-- Try to decode file
local bottom_up
local file_size, width, height, bit_depth, data_size, data, row_size = bmp_decode(filename)
if not file_size then
file_size, width, height, bit_depth, data_size, data, row_size = png_decode(filename)
if not file_size then
error("couldn't decode file", 2)
elseif type(file_size) == "string" then
error(file_size, 2)
else
bottom_up = false
end
elseif type(file_size) == "string" then
error(file_size, 2)
else
bottom_up = height >= 0
height = math.abs(height)
end
-- Return bitmap object
local obj
obj = {
file_size = function()
return file_size
end,
width = function()
return width
end,
height = function()
return height
end,
bit_depth = function()
return bit_depth
end,
data_size = function()
return data_size
end,
row_size = function()
return row_size
end,
bottom_up = function()
return bottom_up
end,
data_raw = function()
return data
end,
data_packed = function()
local data_packed, data_packed_n = {}, 0
local first_row, last_row, row_step
if bottom_up then
first_row, last_row, row_step = height-1, 0, -1
else
first_row, last_row, row_step = 0, height-1, 1
end
if bit_depth == 24 then
local last_row_item, r, g, b = (width-1)*3
for y=first_row, last_row, row_step do
y = 1 + y * row_size
for x=0, last_row_item, 3 do
b, g, r = data:byte(y+x, y+x+2)
data_packed_n = data_packed_n + 1
data_packed[data_packed_n] = {
r = r,
g = g,
b = b,
a = 255
}
end
end
else -- bit_depth == 32
local last_row_item, r, g, b, a = (width-1)*4
for y=first_row, last_row, row_step do
y = 1 + y * row_size
for x=0, last_row_item, 4 do
b, g, r, a = data:byte(y+x, y+x+3)
data_packed_n = data_packed_n + 1
data_packed[data_packed_n] = {
r = r,
g = g,
b = b,
a = a
}
end
end
end
return data_packed
end,
data_text = function()
local data_pack, text, text_n = obj.data_packed(), {"{\\bord0\\shad0\\an7\\p1}"}, 1
local x, y, off_x, chunk_size, color1, color2 = 0, 0, 0
local i, n = 1, #data_pack
while i <= n do
if x == width then
x = 0
y = y + 1
off_x = off_x - width
end
chunk_size, color1, text_n = 1, data_pack[i], text_n + 1
if color1.a == 0 then
for xx=x+1, width-1 do
color2 = data_pack[i+(xx-x)]
if not (color2 and color2.a == 0) then
break
end
chunk_size = chunk_size + 1
end
text[text_n] = string.format("{}m %d %d l %d %d", off_x, y, off_x+chunk_size, y+1)
else
for xx=x+1, width-1 do
color2 = data_pack[i+(xx-x)]
if not (color2 and color1.r == color2.r and color1.g == color2.g and color1.b == color2.b and color1.a == color2.a) then
break
end
chunk_size = chunk_size + 1
end
text[text_n] = string.format("{\\c&H%02X%02X%02X&\\1a&H%02X&}m %d %d l %d %d %d %d %d %d",
color1.b, color1.g, color1.r, 255-color1.a, off_x, y, off_x+chunk_size, y, off_x+chunk_size, y+1, off_x, y+1)
end
i, x = i + chunk_size, x + chunk_size
end
return table.concat(text)
end
}
return obj
end,
-- Create WAV file reader
create_wav_reader = function(filename)
-- Check argument
if type(filename) ~= "string" then
error("audio filename expected", 2)
end
-- Open file handle
local file = io.open(filename, "rb")
if not file then
error("couldn't open file", 2)
end
-- Read file header
local header = file:read(12)
if not header or #header ~= 12 then
error("couldn't read file header", 2)
-- Check WAVE signature
elseif header:sub(1,4) ~= "RIFF" or header:sub(9,12) ~= "WAVE" then
error("not a wave file", 2)
end
-- Data to save (+ read relevant file header field)
local file_size, channels_number, sample_rate, byte_rate, block_align, bits_per_sample = bton(header:sub(5,8)) + 8 -- remaining + already read bytes
local data_begin, data_end
-- Read file chunks
local chunk_type, chunk_size
while true do
-- Read single chunk
chunk_type, chunk_size = file:read(4), file:read(4)
if not chunk_size or #chunk_size ~= 4 then
break
end
chunk_size = bton(chunk_size)
-- Identify chunk type
if chunk_type == "fmt " then
-- Read format informations
header = file:read(16)
if chunk_size < 16 or not header or #header ~= 16 then
error("format chunk corrupted", 2)
elseif bton(header:sub(1,2)) ~= 1 then
error("data must be in PCM format", 2)
end
channels_number, sample_rate, byte_rate, block_align, bits_per_sample = bton(header:sub(3,4)), bton(header:sub(5,8)), bton(header:sub(9,12)), bton(header:sub(13,14)), bton(header:sub(15,16))
if bits_per_sample ~= 8 and bits_per_sample ~= 16 and bits_per_sample ~= 24 and bits_per_sample ~= 32 then
error("bits per sample must be 8, 16, 24 or 32", 2)
elseif channels_number == 0 or sample_rate == 0 or byte_rate == 0 or block_align == 0 then
error("invalid format data", 2)
end
file:seek("cur", chunk_size-16)
elseif chunk_type == "data" then
-- Save samples reference
data_begin = file:seek()
data_end = data_begin + chunk_size
file:seek("cur", chunk_size)
else
-- Skip chunk
file:seek("cur", chunk_size)
end
end
-- Check all needed data are read
if not bits_per_sample or not data_end then
error("format or data are missing", 2)
end
-- Calculate extra data
local samples_per_channel = (data_end - data_begin) / block_align
-- Set file pointer ready for data reading
file:seek("set", data_begin)
-- Return wave object
local obj
obj = {
file_size = function()
return file_size
end,
channels_number = function()
return channels_number
end,
sample_rate = function()
return sample_rate
end,
byte_rate = function()
return byte_rate
end,
block_align = function()
return block_align
end,
bits_per_sample = function()
return bits_per_sample
end,
samples_per_channel = function()
return samples_per_channel
end,
min_max_amplitude = function()
local half_level = 2^bits_per_sample / 2
return -half_level, half_level-1
end,
sample_from_ms = function(ms)
if type(ms) ~= "number" or ms < 0 then
error("positive number expected", 2)
end
return ms * 0.001 * sample_rate
end,
ms_from_sample = function(sample)
if type(sample) ~= "number" or sample < 0 then
error("positive number expected", 2)
end
return sample / sample_rate * 1000
end,
position = function(pos)
if pos ~= nil and (type(pos) ~= "number" or pos < 0) then
error("optional positive number expected", 2)
elseif pos then
file:seek("set", data_begin + pos * block_align)
end
return (file:seek() - data_begin) / block_align
end,
samples_interlaced = function(n)
if type(n) ~= "number" or math.floor(n) < 1 then
error("positive number greater-equal one expected", 2)
end
local output, bytes = {n = 0}, file:read(math.floor(n) * block_align)
if bytes then
local bytes_per_sample, sample = bits_per_sample / 8
local max_amplitude, amplitude_fix = ({127, 32767, 8388607, 2147483647})[bytes_per_sample], ({256, 65536, 16777216, 4294967296})[bytes_per_sample]
for i=1, #bytes, bytes_per_sample do
sample = bton(bytes:sub(i,i+bytes_per_sample-1))
output.n = output.n + 1
output[output.n] = sample > max_amplitude and sample - amplitude_fix or sample
end
end
return output
end,
samples = function(n)
local success, samples = pcall(obj.samples_interlaced, n)
if not success then
error(samples, 2)
end
local output, channel_samples = {n = channels_number}
for c=1, output.n do
channel_samples = {n = math.floor(samples.n / channels_number)}
for s=1, channel_samples.n do
channel_samples[s] = samples[c + (s-1) * channels_number]
end
output[c] = channel_samples
end
return output
end
}
return obj
end,
create_frequency_analyzer = function(samples, sample_rate)
-- Check arguments
if type(samples) ~= "table" or type(sample_rate) ~= "number" or sample_rate < 2 or sample_rate % 2 ~= 0 then
error("samples table and sample rate expected", 2)
end
local samples_n = #samples
if samples_n < 2 then
error("not enough samples", 2)
end
local sample
for i=1, samples_n do
sample = samples[i]
if type(sample) ~= "number" then
error("samples have to be numbers", 2)
elseif sample < -1 or sample > 1 then
error("samples have to be in range -1 <> 1", 2)
end
end
-- Fix samples number to power of 2 for further processing
samples_n = 2^math.floor(math.log(samples_n, 2))
-- Complex numbers
local complex_t
do
local complex = {}
complex_t = function(r, i)
return setmetatable({r = r, i = i}, complex)
end
local function tocomplex(a, b)
if getmetatable(a) ~= complex then return {r = a, i = 0}, b
elseif getmetatable(b) ~= complex then return a, {r = b, i = 0}
else return a, b end
end
complex.__add = function(a, b)
local c1, c2 = tocomplex(a, b)
return complex_t(c1.r + c2.r, c1.i + c2.i)
end
complex.__sub = function(a, b)
local c1, c2 = tocomplex(a, b)
return complex_t(c1.r - c2.r, c1.i - c2.i)
end
complex.__mul = function(a, b)
local c1, c2 = tocomplex(a, b)
return complex_t(c1.r * c2.r - c1.i * c2.i, c1.r * c2.i + c1.i * c2.r)
end
end
local function polar(theta)
return complex_t(math.cos(theta), math.sin(theta))
end
local function magnitude(c)
return math.sqrt(c.r^2 + c.i^2)
end
-- Fast Fourier Transformation
local function fft(x)
-- Check recursion break
local N = x.n
if N > 1 then
-- Divide
local even, odd = {n = 0}, {n = 0}
for i=1, N, 2 do
even.n = even.n + 1
even[even.n] = x[i]
end
for i=2, N, 2 do
odd.n = odd.n + 1
odd[odd.n] = x[i]
end
-- Conquer
fft(even)
fft(odd)
--Combine
local t
for k = 1, N/2 do
t = polar(-2 * math.pi * (k-1) / N) * odd[k]
x[k] = even[k] + t
x[k+N/2] = even[k] - t
end
end
end
-- Samples to complex numbers
local data = {n = samples_n}
for i = 1, data.n do
data[i] = complex_t(samples[i], 0)
end
-- Process FFT
fft(data)
-- Complex numbers to frequencies domain data
for i = 1, data.n do
data[i] = magnitude(data[i])
end
-- Extract frequencies weights
local frequencies, frequency_sum, sample_rate_half = {n = data.n / 2}, 0, sample_rate / 2
for i=1, frequencies.n do
frequency_sum = frequency_sum + data[i]
end
if frequency_sum == 0 then
frequencies[1] = {freq = 0, weight = 1}
for i=2, frequencies.n do
frequencies[i] = {freq = (i-1) / (frequencies.n-1) * sample_rate_half, weight = 0}
end
else
for i=1, frequencies.n do
frequencies[i] = {freq = (i-1) / (frequencies.n-1) * sample_rate_half, weight = data[i] / frequency_sum}
end
end
-- Return frequencies object
return {
frequencies = function()
return Yutils.table.copy(frequencies)
end,
frequency_weight = function(freq)
if type(freq) ~= "number" or freq < 0 or freq > sample_rate_half then
error("valid frequency expected", 2)
end
local frequency
for i=1, frequencies.n do
frequency = frequencies[i]
if frequency.freq == freq then
return frequency.weight
elseif frequency.freq > freq then
local frequency_last = frequencies[i-1]
return (freq - frequency_last.freq) / (frequency.freq - frequency_last.freq) * (frequency.weight - frequency_last.weight) + frequency_last.weight
end
end
end,
frequency_range_weight = function(freq_min, freq_max)
if type(freq_min) ~= "number" or freq_min < 0 or freq_min > sample_rate_half or
type(freq_max) ~= "number" or freq_max < 0 or freq_max > sample_rate_half or
freq_min > freq_max then
error("valid frequencies expected", 2)
end
local weight_sum, frequency = 0
for i=1, frequencies.n do
frequency = frequencies[i]
if frequency.freq >= freq_min then
if frequency.freq <= freq_max then
weight_sum = weight_sum + frequency.weight
else
break
end
end
end
return weight_sum
end
}
end,
-- Creates font
create_font = function(family, bold, italic, underline, strikeout, size, xscale, yscale, hspace)
-- Check arguments
if type(family) ~= "string" or type(bold) ~= "boolean" or type(italic) ~= "boolean" or type(underline) ~= "boolean" or type(strikeout) ~= "boolean" or type(size) ~= "number" or size <= 0 or
(xscale ~= nil and type(xscale) ~= "number") or (yscale ~= nil and type(yscale) ~= "number") or (hspace ~= nil and type(hspace) ~= "number") then
error("expected family, bold, italic, underline, strikeout, size and optional horizontal & vertical scale and intercharacter space", 2)
end
-- Set optional arguments (if not already)
if not xscale then
xscale = 1
end
if not yscale then
yscale = 1
end
if not hspace then
hspace = 0
end
-- Font scale values for increased size & later downscaling to produce floating point coordinates
local upscale = FONT_PRECISION
local downscale = 1 / upscale
-- Body by operation system
if ffi.os == "Windows" then
-- Create device context and set light resources deleter
local resources_deleter
local dc = ffi.gc(ffi.C.CreateCompatibleDC(nil), function() resources_deleter() end)
-- Set context coordinates mapping mode
ffi.C.SetMapMode(dc, ffi.C.MM_TEXT)
-- Set context backgrounds to transparent
ffi.C.SetBkMode(dc, ffi.C.TRANSPARENT)
-- Convert family from utf8 to utf16
family = utf8_to_utf16(family)
if ffi.C.wcslen(family) > 31 then
error("family name to long", 2)
end
-- Create font handle
local font = ffi.C.CreateFontW(
size * upscale, -- nHeight
0, -- nWidth
0, -- nEscapement
0, -- nOrientation
bold and ffi.C.FW_BOLD or ffi.C.FW_NORMAL, -- fnWeight
italic and 1 or 0, -- fdwItalic
underline and 1 or 0, --fdwUnderline
strikeout and 1 or 0, -- fdwStrikeOut
ffi.C.DEFAULT_CHARSET, -- fdwCharSet
ffi.C.OUT_TT_PRECIS, -- fdwOutputPrecision
ffi.C.CLIP_DEFAULT_PRECIS, -- fdwClipPrecision
ffi.C.ANTIALIASED_QUALITY, -- fdwQuality
ffi.C.DEFAULT_PITCH + ffi.C.FF_DONTCARE, -- fdwPitchAndFamily
family
)
-- Set new font to device context
local old_font = ffi.C.SelectObject(dc, font)
-- Define light resources deleter
resources_deleter = function()
ffi.C.SelectObject(dc, old_font)
ffi.C.DeleteObject(font)
ffi.C.DeleteDC(dc)
end
-- Return font object
return {
-- Get font metrics
metrics = function()
-- Get font metrics from device context
local metrics = ffi.new("TEXTMETRICW[1]")
ffi.C.GetTextMetricsW(dc, metrics)
return {
height = metrics[0].tmHeight * downscale * yscale,
ascent = metrics[0].tmAscent * downscale * yscale,
descent = metrics[0].tmDescent * downscale * yscale,
internal_leading = metrics[0].tmInternalLeading * downscale * yscale,
external_leading = metrics[0].tmExternalLeading * downscale * yscale
}
end,
-- Get text extents
text_extents = function(text)
-- Check argument
if type(text) ~= "string" then
error("text expected", 2)
end
-- Get utf16 text
text = utf8_to_utf16(text)
local text_len = ffi.C.wcslen(text)
-- Get text extents with this font
local size = ffi.new("SIZE[1]")
ffi.C.GetTextExtentPoint32W(dc, text, text_len, size)
return {
width = (size[0].cx * downscale + hspace * text_len) * xscale,
height = size[0].cy * downscale * yscale
}
end,
-- Converts text to ASS shape
text_to_shape = function(text)
-- Check argument
if type(text) ~= "string" then
error("text expected", 2)
end
-- Initialize shape as table
local shape, shape_n = {}, 0
-- Get utf16 text
text = utf8_to_utf16(text)
local text_len = ffi.C.wcslen(text)
-- Add path to device context
if text_len > 8192 then
error("text too long", 2)
end
local char_widths
if hspace ~= 0 then
char_widths = ffi.new("INT[?]", text_len)
local size, space = ffi.new("SIZE[1]"), hspace * upscale
for i=0, text_len-1 do
ffi.C.GetTextExtentPoint32W(dc, text+i, 1, size)
char_widths[i] = size[0].cx + space
end
end
ffi.C.BeginPath(dc)
ffi.C.ExtTextOutW(dc, 0, 0, 0x0, nil, text, text_len, char_widths)
ffi.C.EndPath(dc)
-- Get path data
local points_n = ffi.C.GetPath(dc, nil, nil, 0)
if points_n > 0 then
local points, types = ffi.new("POINT[?]", points_n), ffi.new("BYTE[?]", points_n)
ffi.C.GetPath(dc, points, types, points_n)
-- Convert points to shape
local i, last_type, cur_type, cur_point = 0
while i < points_n do
cur_type, cur_point = types[i], points[i]
if cur_type == ffi.C.PT_MOVETO then
if last_type ~= ffi.C.PT_MOVETO then
shape_n = shape_n + 1
shape[shape_n] = "m"
last_type = cur_type
end
shape[shape_n+1] = Yutils.math.round(cur_point.x * downscale * xscale, FP_PRECISION)
shape[shape_n+2] = Yutils.math.round(cur_point.y * downscale * yscale, FP_PRECISION)
shape_n = shape_n + 2
i = i + 1
elseif cur_type == ffi.C.PT_LINETO or cur_type == (ffi.C.PT_LINETO + ffi.C.PT_CLOSEFIGURE) then
if last_type ~= ffi.C.PT_LINETO then
shape_n = shape_n + 1
shape[shape_n] = "l"
last_type = cur_type
end
shape[shape_n+1] = Yutils.math.round(cur_point.x * downscale * xscale, FP_PRECISION)
shape[shape_n+2] = Yutils.math.round(cur_point.y * downscale * yscale, FP_PRECISION)
shape_n = shape_n + 2
i = i + 1
elseif cur_type == ffi.C.PT_BEZIERTO or cur_type == (ffi.C.PT_BEZIERTO + ffi.C.PT_CLOSEFIGURE) then
if last_type ~= ffi.C.PT_BEZIERTO then
shape_n = shape_n + 1
shape[shape_n] = "b"
last_type = cur_type
end
shape[shape_n+1] = Yutils.math.round(cur_point.x * downscale * xscale, FP_PRECISION)
shape[shape_n+2] = Yutils.math.round(cur_point.y * downscale * yscale, FP_PRECISION)
shape[shape_n+3] = Yutils.math.round(points[i+1].x * downscale * xscale, FP_PRECISION)
shape[shape_n+4] = Yutils.math.round(points[i+1].y * downscale * yscale, FP_PRECISION)
shape[shape_n+5] = Yutils.math.round(points[i+2].x * downscale * xscale, FP_PRECISION)
shape[shape_n+6] = Yutils.math.round(points[i+2].y * downscale * yscale, FP_PRECISION)
shape_n = shape_n + 6
i = i + 3
else -- invalid type (should never happen, but let us be safe)
i = i + 1
end
if cur_type % 2 == 1 then -- odd = PT_CLOSEFIGURE
shape_n = shape_n + 1
shape[shape_n] = "c"
end
end
end
-- Clear device context path
ffi.C.AbortPath(dc)
-- Return shape as string
return table.concat(shape, " ")
end
}
else -- Unix
-- Check whether or not the pangocairo library was loaded
if not pangocairo then
error("pangocairo library couldn't be loaded", 2)
end
-- Create surface, context & layout
local surface = pangocairo.cairo_image_surface_create(ffi.C.CAIRO_FORMAT_A8, 1, 1)
local context = pangocairo.cairo_create(surface)
local layout
layout = ffi.gc(pangocairo.pango_cairo_create_layout(context), function()
pangocairo.g_object_unref(layout)
pangocairo.cairo_destroy(context)
pangocairo.cairo_surface_destroy(surface)
end)
-- Set font to layout
local font_desc = ffi.gc(pangocairo.pango_font_description_new(), pangocairo.pango_font_description_free)
pangocairo.pango_font_description_set_family(font_desc, family)
pangocairo.pango_font_description_set_weight(font_desc, bold and ffi.C.PANGO_WEIGHT_BOLD or ffi.C.PANGO_WEIGHT_NORMAL)
pangocairo.pango_font_description_set_style(font_desc, italic and ffi.C.PANGO_STYLE_ITALIC or ffi.C.PANGO_STYLE_NORMAL)
pangocairo.pango_font_description_set_absolute_size(font_desc, size * ffi.C.PANGO_SCALE * upscale)
pangocairo.pango_layout_set_font_description(layout, font_desc)
local attr = ffi.gc(pangocairo.pango_attr_list_new(), pangocairo.pango_attr_list_unref)
pangocairo.pango_attr_list_insert(attr, pangocairo.pango_attr_underline_new(underline and ffi.C.PANGO_UNDERLINE_SINGLE or ffi.C.PANGO_UNDERLINE_NONE))
pangocairo.pango_attr_list_insert(attr, pangocairo.pango_attr_strikethrough_new(strikeout))
pangocairo.pango_layout_set_attributes(layout, attr)
-- Scale factor for resulting font data
local fonthack_scale
if LIBASS_FONTHACK then
local metrics = ffi.gc(pangocairo.pango_context_get_metrics(pangocairo.pango_layout_get_context(layout), pangocairo.pango_layout_get_font_description(layout), nil), pangocairo.pango_font_metrics_unref)
fonthack_scale = size / ((pangocairo.pango_font_metrics_get_ascent(metrics) + pangocairo.pango_font_metrics_get_descent(metrics)) / ffi.C.PANGO_SCALE * downscale)
else
fonthack_scale = 1
end
-- Return font object
return {
-- Get font metrics
metrics = function()
local metrics = ffi.gc(pangocairo.pango_context_get_metrics(pangocairo.pango_layout_get_context(layout), pangocairo.pango_layout_get_font_description(layout), nil), pangocairo.pango_font_metrics_unref)
local ascent, descent = pangocairo.pango_font_metrics_get_ascent(metrics) / ffi.C.PANGO_SCALE * downscale,
pangocairo.pango_font_metrics_get_descent(metrics) / ffi.C.PANGO_SCALE * downscale
return {
height = (ascent + descent) * yscale * fonthack_scale,
ascent = ascent * yscale * fonthack_scale,
descent = descent * yscale * fonthack_scale,
internal_leading = 0,
external_leading = pangocairo.pango_layout_get_spacing(layout) / ffi.C.PANGO_SCALE * downscale * yscale * fonthack_scale
}
end,
-- Get text extents
text_extents = function(text)
-- Check argument
if type(text) ~= "string" then
error("text dexpected", 2)
end
local function get_rect(new_text)
-- Set text to layout
pangocairo.pango_layout_set_text(layout, new_text, -1)
-- Get text extents with this font
local rect = ffi.new("PangoRectangle[1]")
pangocairo.pango_layout_get_pixel_extents(layout, nil, rect)
return rect[0]
end
local rect_width = 0
for c in text:gmatch(".") do
rect_width = rect_width + get_rect(c).width
end
local new_width = 0
if #text ~= 0 then
new_width = (rect_width * downscale * fonthack_scale + hspace * (#text - 1)) * xscale
end
return {
width = new_width,
height = get_rect(text).height * downscale * yscale * fonthack_scale
}
end,
-- Converts text to ASS shape
text_to_shape = function(text)
-- Check argument
if type(text) ~= "string" then
error("text expected", 2)
end
-- Set text path to layout
pangocairo.cairo_save(context)
pangocairo.cairo_scale(context, downscale * xscale * fonthack_scale, downscale * yscale * fonthack_scale)
pangocairo.pango_layout_set_text(layout, text, -1)
pangocairo.pango_cairo_layout_path(context, layout)
pangocairo.cairo_restore(context)
-- Initialize shape as table
local shape, shape_n = {}, 0
-- Convert path to shape
local path = ffi.gc(pangocairo.cairo_copy_path(context), pangocairo.cairo_path_destroy)
if(path[0].status == ffi.C.CAIRO_STATUS_SUCCESS) then
local i, cur_type, last_type = 0
while(i < path[0].num_data) do
cur_type = path[0].data[i].header.type
if cur_type == ffi.C.CAIRO_PATH_MOVE_TO then
if cur_type ~= last_type then
shape_n = shape_n + 1
shape[shape_n] = "m"
end
shape[shape_n+1] = Yutils.math.round(path[0].data[i+1].point.x, FP_PRECISION)
shape[shape_n+2] = Yutils.math.round(path[0].data[i+1].point.y, FP_PRECISION)
shape_n = shape_n + 2
elseif cur_type == ffi.C.CAIRO_PATH_LINE_TO then
if cur_type ~= last_type then
shape_n = shape_n + 1
shape[shape_n] = "l"
end
shape[shape_n+1] = Yutils.math.round(path[0].data[i+1].point.x, FP_PRECISION)
shape[shape_n+2] = Yutils.math.round(path[0].data[i+1].point.y, FP_PRECISION)
shape_n = shape_n + 2
elseif cur_type == ffi.C.CAIRO_PATH_CURVE_TO then
if cur_type ~= last_type then
shape_n = shape_n + 1
shape[shape_n] = "b"
end
shape[shape_n+1] = Yutils.math.round(path[0].data[i+1].point.x, FP_PRECISION)
shape[shape_n+2] = Yutils.math.round(path[0].data[i+1].point.y, FP_PRECISION)
shape[shape_n+3] = Yutils.math.round(path[0].data[i+2].point.x, FP_PRECISION)
shape[shape_n+4] = Yutils.math.round(path[0].data[i+2].point.y, FP_PRECISION)
shape[shape_n+5] = Yutils.math.round(path[0].data[i+3].point.x, FP_PRECISION)
shape[shape_n+6] = Yutils.math.round(path[0].data[i+3].point.y, FP_PRECISION)
shape_n = shape_n + 6
elseif cur_type == ffi.C.CAIRO_PATH_CLOSE_PATH then
if cur_type ~= last_type then
shape_n = shape_n + 1
shape[shape_n] = "c"
end
end
last_type = cur_type
i = i + path[0].data[i].header.length
end
end
pangocairo.cairo_new_path(context)
return table.concat(shape, " ")
end
}
end
end,
-- Lists available system fonts
list_fonts = function(with_filenames)
-- Check argument
if with_filenames ~= nil and type(with_filenames) ~= "boolean" then
error("optional boolean expected", 2)
end
-- Output fonts buffer
local fonts = {n = 0}
-- Body by operation system
if ffi.os == "Windows" then
-- Enumerate font families (of all charsets)
local plogfont = ffi.new("LOGFONTW[1]")
plogfont[0].lfCharSet = ffi.C.DEFAULT_CHARSET
plogfont[0].lfFaceName[0] = 0 -- Empty string
plogfont[0].lfPitchAndFamily = ffi.C.DEFAULT_PITCH + ffi.C.FF_DONTCARE
local fontname, style, font
ffi.C.EnumFontFamiliesExW(ffi.gc(ffi.C.CreateCompatibleDC(nil), ffi.C.DeleteDC), plogfont, function(penumlogfont, _, fonttype, _)
-- Skip different font charsets
fontname, style = utf16_to_utf8(penumlogfont[0].elfLogFont.lfFaceName), utf16_to_utf8(penumlogfont[0].elfStyle)
for i=1, fonts.n do
font = fonts[i]
if font.name == fontname and font.style == style then
goto win_font_found
end
end
-- Add font entry
fonts.n = fonts.n + 1
fonts[fonts.n] = {
name = fontname,
longname = utf16_to_utf8(penumlogfont[0].elfFullName),
style = style,
type = fonttype == ffi.C.FONTTYPE_RASTER and "Raster" or fonttype == ffi.C.FONTTYPE_DEVICE and "Device" or fonttype == ffi.C.FONTTYPE_TRUETYPE and "TrueType" or "Unknown",
}
::win_font_found::
-- Continue enumeration (till end)
return 1
end, 0, 0)
-- Files to fonts?
if with_filenames then
-- Adds filename to fitting font
local function file_to_font(fontname, fontfile)
for i=1, fonts.n do
font = fonts[i]
if fontname == font.name:gsub("^@", "", 1) or fontname == string.format("%s %s", font.name:gsub("^@", "", 1), font.style) or fontname == font.longname:gsub("^@", "", 1) then
font.file = fontfile
end
end
end
-- Search registry for font files
local pregkey, fontfile = ffi.new("HKEY[1]")
if advapi.RegOpenKeyExA(ffi.cast("HKEY", ffi.C.HKEY_LOCAL_MACHINE), "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", 0, ffi.C.KEY_READ, pregkey) == ffi.C.ERROR_SUCCESS then
local regkey = ffi.gc(pregkey[0], advapi.RegCloseKey)
local value_index, value_name, pvalue_name_size, value_data, pvalue_data_size = 0, ffi.new("wchar_t[16383]"), ffi.new("DWORD[1]"), ffi.new("BYTE[65536]"), ffi.new("DWORD[1]")
while true do
pvalue_name_size[0], pvalue_data_size[0] = ffi.sizeof(value_name) / ffi.sizeof("wchar_t"), ffi.sizeof(value_data)
if advapi.RegEnumValueW(regkey, value_index, value_name, pvalue_name_size, nil, nil, value_data, pvalue_data_size) ~= ffi.C.ERROR_SUCCESS then
break
else
value_index = value_index + 1
end
fontname = utf16_to_utf8(value_name):gsub("(.*) %(.-%)$", "%1", 1)
fontfile = utf16_to_utf8(ffi.cast("wchar_t*", value_data))
file_to_font(fontname, fontfile)
if fontname:find(" & ") then
for fontname in fontname:gmatch("(.-) & ") do
file_to_font(fontname, fontfile)
end
file_to_font(fontname:match(".* & (.-)$"), fontfile)
end
end
end
end
else -- Unix
-- Check whether or not the fontconfig library was loaded
if not fontconfig then
error("fontconfig library couldn't be loaded", 2)
end
-- Get fonts list from fontconfig
local fontset = ffi.gc(fontconfig.FcFontList(fontconfig.FcInitLoadConfigAndFonts(),
ffi.gc(fontconfig.FcPatternCreate(), fontconfig.FcPatternDestroy),
ffi.gc(fontconfig.FcObjectSetBuild("family", "fullname", "style", "outline", with_filenames and "file" or nil, nil), fontconfig.FcObjectSetDestroy)),
fontconfig.FcFontSetDestroy)
-- Enumerate fonts
local font, family, fullname, style, outline, file
local cstr, cbool = ffi.new("FcChar8*[1]"), ffi.new("FcBool[1]")
for i=0, fontset[0].nfont-1 do
-- Get font informations
font = fontset[0].fonts[i]
family, fullname, style, outline, file = nil
if fontconfig.FcPatternGetString(font, "family", 0, cstr) == ffi.C.FcResultMatch then
family = ffi.string(cstr[0])
end
if fontconfig.FcPatternGetString(font, "fullname", 0, cstr) == ffi.C.FcResultMatch then
fullname = ffi.string(cstr[0])
end
if fontconfig.FcPatternGetString(font, "style", 0, cstr) == ffi.C.FcResultMatch then
style = ffi.string(cstr[0])
end
if fontconfig.FcPatternGetBool(font, "outline", 0, cbool) == ffi.C.FcResultMatch then
outline = cbool[0]
end
if fontconfig.FcPatternGetString(font, "file", 0, cstr) == ffi.C.FcResultMatch then
file = ffi.string(cstr[0])
end
-- Add font entry
if family and fullname and style and outline then
fonts.n = fonts.n + 1
fonts[fonts.n] = {
name = family,
longname = fullname,
style = style,
type = outline == 0 and "Raster" or "Outline",
file = file
}
end
end
end
-- Order fonts by name & style
table.sort(fonts, function(font1, font2)
if font1.name == font2.name then
return font1.style < font2.style
else
return font1.name < font2.name
end
end)
-- Return collected fonts
return fonts
end
}
}
-- Put library in global scope (if first script argument is true)
if ({...})[1] then
_G.Yutils = Yutils
end
-- Return library to script loader
return Yutils
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment