├── .gitignore ├── tests ├── test.bmp ├── test.png ├── test.wav ├── table.lua ├── utf8.lua ├── algorithm.lua ├── bmp.lua ├── font.lua ├── math.lua ├── wav.lua ├── ass.lua ├── shape.lua └── macro.lua ├── docs ├── favicon.png ├── text_align.jpg ├── text_extents.png ├── text_sections.jpg ├── style.css ├── content_format.js └── index.html ├── .gitmodules ├── .gitattributes ├── README.md └── src └── Yutils.lua /.gitignore: -------------------------------------------------------------------------------- 1 | TODO/ 2 | tests/libpng.dll -------------------------------------------------------------------------------- /tests/test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/tests/test.bmp -------------------------------------------------------------------------------- /tests/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/tests/test.png -------------------------------------------------------------------------------- /tests/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/tests/test.wav -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/docs/favicon.png -------------------------------------------------------------------------------- /docs/text_align.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/docs/text_align.jpg -------------------------------------------------------------------------------- /docs/text_extents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/docs/text_extents.png -------------------------------------------------------------------------------- /docs/text_sections.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Youka/Yutils/HEAD/docs/text_sections.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "luajit"] 2 | path = deps/luajit 3 | url = https://github.com/LuaDist/luajit 4 | [submodule "libpng"] 5 | path = deps/libpng 6 | url = http://git.code.sf.net/p/libpng/code 7 | -------------------------------------------------------------------------------- /tests/table.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | local t = {a = 1, {foo = "bar"}} 4 | print(Yutils.table.tostring(t)) 5 | print(string.format("Origin %s <> Copy %s", t, Yutils.table.copy(t, 1))) -------------------------------------------------------------------------------- /tests/utf8.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | local s = "Äの" 4 | print("Length: " .. Yutils.utf8.len(s)) 5 | for ci, char in Yutils.utf8.chars(s) do 6 | print(string.format("%d: %s (%d)", ci, char, Yutils.utf8.charrange(char,1))) 7 | end -------------------------------------------------------------------------------- /tests/algorithm.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | for s, e, i, n in Yutils.algorithm.frames(0, 10, 1.5) do 4 | print(string.format("Start: %.1f - End: %.1f - Index: %d - Max: %d", s, e, i, n)) 5 | end 6 | for line in Yutils.algorithm.lines("Hello\n\r\n\rworld!") do 7 | print(string.format("#%d: %s", #line, line)) 8 | end -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Git files 5 | .gitattributes text 6 | .gitignore text 7 | .gitmodules text 8 | 9 | # Documentations 10 | *.md text 11 | *.html diff=html 12 | *.css diff=css 13 | 14 | # Source code 15 | *.lua diff=lua 16 | *.js diff=js 17 | 18 | # Images 19 | *.bmp binary 20 | *.png binary 21 | 22 | # Audio 23 | *.wav binary -------------------------------------------------------------------------------- /tests/bmp.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | local bmp = Yutils.decode.create_bmp_reader("test.bmp") 4 | print("File size: " .. bmp.file_size()) 5 | print("Width: " .. bmp.width()) 6 | print("Height: " .. bmp.height()) 7 | print("Depth: " .. bmp.bit_depth()) 8 | print("Data size: " .. bmp.data_size()) 9 | print("Row size: " .. bmp.row_size()) 10 | print("Rows go bottom-up: ", bmp.bottom_up()) 11 | print("Packed data:\n" .. Yutils.table.tostring(bmp.data_packed())) 12 | print("Data text: " .. bmp.data_text()) -------------------------------------------------------------------------------- /tests/font.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | local font = Yutils.decode.create_font("Comic Sans MS", true, true, true, false, 64, 1, 1, 0.5) 4 | print("Metrics:\n" .. Yutils.table.tostring(font.metrics())) 5 | print("Extents:\n" .. Yutils.table.tostring(font.text_extents("TestのMy"))) 6 | print("Text shape: " .. font.text_to_shape("TestのMy")) 7 | print("Fonts:") 8 | for _, font in ipairs(Yutils.decode.list_fonts(true)) do 9 | print(string.format("\t%s: %s (%s)", font.name, font.style, font.file)) 10 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yutils - an ASS typeset utilities library 2 | ========================================= 3 | Requirements: 4 | ------------- 5 | * LuaJIT (*interpreter for Lua scripts, using this library*) 6 | * libpng (*PNG image decoder for library bitmap reader*) [*optional*] 7 | 8 | Files: 9 | ------ 10 | * deps/ (*folder with project dependencies*) 11 | * docs/ (*folder with project documentations*) 12 | * src/ (*folder with library modules*) 13 | * tests/ (*folder with example scripts of library usage*) 14 | * .gitattributes (*how Git should handle files*) 15 | * .gitignore (*files to ignore by Git*) 16 | * .gitmodules (*submodules of this Git project*) 17 | * README.md (*this file [you're currently reading]*) 18 | 19 | Documentations: 20 | ----- 21 | See **docs/index.html** for descriptions and **tests/\*.lua** for executable examples. -------------------------------------------------------------------------------- /tests/math.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | print("Arc curve: ", Yutils.math.arc_curve(100, 0, 0, 0, 95)) 4 | print("Curve point: ", Yutils.math.bezier(0.5, {{0,0},{4,-10},{8,2},{12,0}})) 5 | print("Distance: ", Yutils.math.distance(10, -5, -3)) 6 | print("Degree: ", Yutils.math.degree(0,-1,0, -0.1,1,0)) 7 | print("Lines intersection: ", Yutils.math.line_intersect(0, 0, 10, 10, 1, 0, 9, 10, true)) 8 | print("Orthogonal: ", Yutils.math.ortho(1,0,0, 0,0,1)) 9 | local mat = Yutils.math.create_matrix().rotate("z", 90).scale(0.5, 2, 2).translate(10, 20, -5) 10 | print("Transformed point: ", mat.transform(0, 0, 0)) 11 | print("Inversed matrix:\n" .. Yutils.table.tostring(mat.inverse().get_data())) 12 | print("Random number: " .. Yutils.math.randomsteps(-1.5, 3, 0.5)) 13 | print("Rounded number: " .. Yutils.math.round(1.2345, 3)) 14 | print("Scaled vector: ", Yutils.math.stretch(1, 0.5, -2, 3)) 15 | print("Trimmed number: " .. Yutils.math.trim(2.1, 0, 2)) -------------------------------------------------------------------------------- /tests/wav.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | local wav = Yutils.decode.create_wav_reader("test.wav") 4 | print("File size: " .. wav.file_size()) 5 | print("Channels number: " .. wav.channels_number()) 6 | print("Sample rate: " .. wav.sample_rate()) 7 | print("Byte rate: " .. wav.byte_rate()) 8 | print("Block align: " .. wav.block_align()) 9 | print("Bits per sample: " .. wav.bits_per_sample()) 10 | print("Samples per channel: " .. wav.samples_per_channel()) 11 | print("Minimal & maximal amplitude: ", wav.min_max_amplitude()) 12 | print("Sample at 120 milliseconds: " .. wav.sample_from_ms(120)) 13 | print("Milliseconds at sample 87: " .. wav.ms_from_sample(87)) 14 | print("Position set to sample " .. wav.position(2)) 15 | print("Read 100 samples:\n" .. Yutils.table.tostring(wav.samples(100))) 16 | 17 | wav.position(0) 18 | local samples = wav.samples(4096)[1] 19 | for i=1, samples.n do 20 | samples[i] = samples[i] / math.abs(wav.min_max_amplitude()) 21 | end 22 | print("Frequencies of some samples from channel 1:\n" .. Yutils.table.tostring(Yutils.decode.create_frequency_analyzer(samples, wav.sample_rate()).frequencies())) -------------------------------------------------------------------------------- /tests/ass.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | print("Converted milliseconds: " .. Yutils.ass.convert_time(1500)) 4 | print("Converted timestamp: " .. Yutils.ass.convert_time("2:03:00.04")) 5 | print("Converted ASS style color+alpha: ", Yutils.ass.convert_coloralpha("&H80FFFF20")) 6 | print("Converted numeric color: " .. Yutils.ass.convert_coloralpha(255, 127, 0)) 7 | print("Interpolated ASS alpha: " .. Yutils.ass.interpolate_coloralpha(0.75, "&H00&", "&H40&", "&HFF&")) 8 | 9 | local parser = Yutils.ass.create_parser([[ 10 | [Script Info] 11 | WrapStyle: 1 12 | ScaledBorderAndShadow: no 13 | PlayResX: 1280 14 | PlayResY: 720 15 | 16 | [V4+ Styles] 17 | Style: Default,Arial,80,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,10,10,10,1 18 | Style: Default2,Arial,90,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,10,10,10,1 19 | 20 | [Events] 21 | Dialogue: 0,0:00:00.00,0:00:10.00,Default,Anyone,0,0,0,First line,Hello 22 | Dialogue: 0,0:00:20.00,0:00:30.00,Default,Someone,0,0,0,Second line,World! 23 | Dialogue: 0,0:00:40.00,0:01:00.00,Default2,He/she/it,0,0,0,Third line,from Yutils! 24 | ]]) 25 | print("Meta:\n" .. Yutils.table.tostring(parser.meta())) 26 | print("Styles:\n" .. Yutils.table.tostring(parser.styles())) 27 | print("Dialogs:\n" .. Yutils.table.tostring(parser.dialogs(true))) -------------------------------------------------------------------------------- /tests/shape.lua: -------------------------------------------------------------------------------- 1 | local Yutils = dofile("../src/Yutils.lua") 2 | 3 | local shape = "m -100.5 0 l 100 0 b 100 100 -100 100 -100.5 0 c" 4 | print("Bounding: ", Yutils.shape.bounding(shape)) 5 | print("Filtered shape: " .. Yutils.shape.filter(shape, function(x, y) 6 | return x / 10, y * 2 7 | end)) 8 | print("Flattened shape: " .. Yutils.shape.flatten(shape)) 9 | print("Moved shape: " .. Yutils.shape.move(shape, 5, -2)) 10 | print("Mirrored shape: " .. Yutils.shape.transform(shape, Yutils.math.create_matrix().rotate("y", 180))) 11 | print("Shape with splitted lines: " .. Yutils.shape.split(shape, 15)) 12 | print("Shape outline: " .. Yutils.shape.to_outline(Yutils.shape.flatten(shape), 5.5, 10, "miter")) 13 | print("Pixels:\n" .. Yutils.table.tostring(Yutils.shape.to_pixels("m -4.5 -4 l 0 -4 0 0"))) 14 | print( 15 | "Detected shapes:\n" .. 16 | Yutils.table.tostring( 17 | Yutils.shape.detect(5, 5, { 18 | 1, 1, 1, 1, 1, 19 | 1, 0, 0, 2, 1, 20 | 1, 0, 1, 0, 1, 21 | 1, 0, 0, 0, 1, 22 | 1, 1, 1, 1, 1 23 | }) 24 | ) 25 | ) 26 | print( 27 | "Text on shape: " .. 28 | Yutils.shape.glue( 29 | Yutils.shape.split(Yutils.shape.flatten(Yutils.decode.create_font("Times New Roman", true, false, true, false, 100).text_to_shape("This is a long text for a test!")), 1), 30 | "m 0 0 b 0 -300 450 -300 450 0 b 450 240 90 240 93 0 b 90 -180 360 -180 360 0 b 360 120 180 120 180 0 b 180 -60 270 -60 270 0", 31 | function(x_pct, y_off) 32 | return 0.2 + x_pct * 0.8, y_off * 1.2 33 | end 34 | ) 35 | ) -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | color: white; 3 | background-color: black; 4 | margin: 0px; 5 | } 6 | 7 | noscript{ 8 | color: white; 9 | background-color: red; 10 | font: bold 24px Arial; 11 | display: block; 12 | text-align: center; 13 | padding: 5px; 14 | border-bottom: ridge 4px white; 15 | } 16 | 17 | h1{ 18 | font-family: Tahoma; 19 | font-size: 40px; 20 | text-align: center; 21 | margin: 0px; 22 | text-shadow: -1px -1px 1px #FF0000, 23 | 1px -1px 1px #FFFF00, 24 | 1px 1px 1px #0000FF, 25 | -1px 1px 1px #00FF00, 26 | 0px -1px 1px #FF8000, 27 | 1px 0px 1px #808080, 28 | 0px 1px 1px #008080, 29 | -1px 0px 1px #808000; 30 | background: #202020; 31 | -webkit-user-select: none; 32 | -khtml-user-select: none; 33 | -moz-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | cursor: default; 37 | } 38 | 39 | h1 span{ 40 | display: block; 41 | font-size: 16px; 42 | } 43 | 44 | hr{ 45 | margin: 0px; 46 | background-color: #A0A0A0; 47 | border: dotted 2px #606060; 48 | } 49 | 50 | a{ 51 | color: black; 52 | text-decoration: none; 53 | } 54 | 55 | div.contents{ 56 | cursor: pointer; 57 | background-color: white; 58 | border: double 6px #404040; 59 | top: 0; 60 | right: 0; 61 | position: fixed; 62 | overflow: auto; 63 | width: 0; 64 | max-height: 90%; 65 | opacity: 0.5; 66 | } 67 | div.contents:hover{ 68 | width: auto; 69 | opacity: 1; 70 | } 71 | 72 | div.contents a.contents, div.contents a.subcontents{ 73 | font-weight: bold; 74 | font-size: 16px; 75 | display: table; 76 | margin: 5px; 77 | } 78 | div.contents a.contents:hover, div.contents a.subcontents:hover{ 79 | text-shadow: 0px 0px 2px black; 80 | } 81 | div.contents a.subcontents{ 82 | padding-left: 18px; 83 | } 84 | 85 | div.section{ 86 | color: black; 87 | background-color: #E0E0E0; 88 | display: table; 89 | margin: 0px 0px 0px 5px; 90 | border: 4px ridge #0808FF; 91 | padding: 4px; 92 | } 93 | 94 | div.section a{ 95 | color: #000080; 96 | } 97 | div.section a:hover{ 98 | color: blue; 99 | } 100 | 101 | span#section{ 102 | font-size: 20px; 103 | font-weight: bold; 104 | text-shadow: -2px -2px 1px #8080FF, 105 | 0px -2px 1px #8080FF, 106 | 2px -2px 1px #000080, 107 | 2px 0px 1px #000080, 108 | 2px 2px 1px #000080, 109 | 0px 2px 1px #000080, 110 | -2px 2px 1px #000080, 111 | -2px 0px 1px #8080FF; 112 | line-height: 0px; 113 | position: relative; 114 | left: 10px; 115 | top: 10px; 116 | } 117 | 118 | div.section div.function{ 119 | margin: 6px; 120 | padding: 5px; 121 | border: dotted 3px grey; 122 | font-size: 14px; 123 | background-color: white; 124 | display: table; 125 | } 126 | div.section div.function span.definition{ 127 | font-weight: bold; 128 | font-size: 18px; 129 | } 130 | div.section div.function span.function_return{ 131 | color: green; 132 | } 133 | div.section div.function span.function_parameters{ 134 | color: red; 135 | } 136 | 137 | ul{ 138 | margin: 0px; 139 | padding-left: 18px; 140 | } 141 | 142 | table.grid{ 143 | border-collapse: collapse; 144 | } 145 | table.grid tr td{ 146 | padding: 0; 147 | text-align: center; 148 | width: 20px; 149 | height: 20px; 150 | border: solid 1px grey; 151 | background-color: black; 152 | color: white; 153 | } 154 | 155 | div.code{ 156 | margin: 5px; 157 | padding: 4px; 158 | white-space: pre; 159 | display: inline-block; 160 | max-height: 300px; 161 | overflow-y: auto; 162 | border: solid 3px black; 163 | background-color: #C0C0C0; 164 | } 165 | div.code:hover{ 166 | margin: 3px; 167 | border: solid 5px black; 168 | background-color: white; 169 | } 170 | 171 | td.linenumbers{ 172 | display: none; 173 | border-left: solid 1px grey; 174 | background-color: #D0D0D0; 175 | } -------------------------------------------------------------------------------- /tests/macro.lua: -------------------------------------------------------------------------------- 1 | -- Script information 2 | script_name = "Wobble text" 3 | script_description = "Converts a text to a shape and adds wobbling." 4 | script_author = "Youka" 5 | script_version = "1.2" 6 | script_modified = "31th July 2014" 7 | 8 | -- Load Yutils library 9 | local Yutils = include("Yutils.lua") 10 | 11 | -- UI configuration template 12 | local config_template = { 13 | { 14 | class = "label", 15 | x = 0, y = 0, width = 1, height = 1, 16 | label = "Fontname: " 17 | }, 18 | { 19 | class = "dropdown", name = "fontname", 20 | x = 1, y = 0, width = 3, height = 1, 21 | hint = "Font family" -- items = {}, value = "" 22 | }, 23 | { 24 | class = "checkbox", name = "bold", 25 | x = 4, y = 0, width = 1, height = 1, 26 | hint = "Text should be bold?", label = "Bold?", value = false 27 | }, 28 | { 29 | class = "checkbox", name = "italic", 30 | x = 5, y = 0, width = 1, height = 1, 31 | hint = "Text should be italic?", label = "Italic?", value = false 32 | }, 33 | { 34 | class = "label", 35 | x = 0, y = 1, width = 1, height = 1, 36 | label = "Fontsize: " 37 | }, 38 | { 39 | class = "intedit", name = "fontsize", 40 | x = 1, y = 1, width = 3, height = 1, 41 | hint = "Font size", value = 30, min = 1, max = 1000 42 | }, 43 | { 44 | class = "checkbox", name = "underline", 45 | x = 4, y = 1, width = 1, height = 1, 46 | hint = "Text should be underlined?", label = "Underline?", value = false 47 | }, 48 | { 49 | class = "checkbox", name = "strikeout", 50 | x = 5, y = 1, width = 1, height = 1, 51 | hint = "Text should be outstriked?", label = "Strikeout?", value = false 52 | }, 53 | { 54 | class = "label", 55 | x = 0, y = 2, width = 1, height = 1, 56 | label = "Scale X%: " 57 | }, 58 | { 59 | class = "floatedit", name = "scale_x", 60 | x = 1, y = 2, width = 1, height = 1, 61 | hint = "Horizontal scaling in percent", value = 100, min = 0.01, max = 10000, step = 0.01 62 | }, 63 | { 64 | class = "label", 65 | x = 2, y = 2, width = 1, height = 1, 66 | label = "Scale Y%: " 67 | }, 68 | { 69 | class = "floatedit", name = "scale_y", 70 | x = 3, y = 2, width = 1, height = 1, 71 | hint = "Vertical scaling in percent", value = 100, min = 0.01, max = 10000, step = 0.01 72 | }, 73 | { 74 | class = "label", 75 | x = 4, y = 2, width = 1, height = 1, 76 | label = "Spacing: " 77 | }, 78 | { 79 | class = "floatedit", name = "spacing", 80 | x = 5, y = 2, width = 1, height = 1, 81 | hint = "Intercharacter spacing", value = 0, min = -100, max = 100, step = 0.01 82 | }, 83 | { 84 | class = "label", 85 | x = 0, y = 3, width = 1, height = 1, 86 | label = "Text:" 87 | }, 88 | { 89 | class = "textbox", name = "text", 90 | x = 0, y = 4, width = 6, height = 2, 91 | hint = "Text to convert", text = "" 92 | }, 93 | { 94 | class = "label", 95 | x = 1, y = 6, width = 1, height = 1, 96 | label = "X" 97 | }, 98 | { 99 | class = "label", 100 | x = 2, y = 6, width = 1, height = 1, 101 | label = "Y" 102 | }, 103 | { 104 | class = "label", 105 | x = 0, y = 7, width = 1, height = 1, 106 | label = "Wobble frequency: " 107 | }, 108 | { 109 | class = "floatedit", name = "wobble_frequency_x", 110 | x = 1, y = 7, width = 1, height = 1, 111 | hint = "Horizontal wobbling frequency in percent", value = 0, min = 0, max = 10, step = 0.00001 112 | }, 113 | { 114 | class = "floatedit", name = "wobble_frequency_y", 115 | x = 2, y = 7, width = 1, height = 1, 116 | hint = "Vertical wobbling frequency in percent", value = 0, min = 0, max = 10, step = 0.00001 117 | }, 118 | { 119 | class = "label", 120 | x = 0, y = 8, width = 1, height = 1, 121 | label = "Wobble strength: " 122 | }, 123 | { 124 | class = "floatedit", name = "wobble_strength_x", 125 | x = 1, y = 8, width = 1, height = 1, 126 | hint = "Horizontal wobbling strength in pixels", value = 0, min = 0, max = 100, step = 0.01 127 | }, 128 | { 129 | class = "floatedit", name = "wobble_strength_y", 130 | x = 2, y = 8, width = 1, height = 1, 131 | hint = "Vertical wobbling strength in pixels", value = 0, min = 0, max = 100, step = 0.01 132 | }, 133 | } 134 | do 135 | -- Fill font families in configuration 136 | local items, items_n = {}, 0 137 | for _, family in ipairs(Yutils.decode.list_fonts()) do 138 | items_n = items_n + 1 139 | items[items_n] = family.name 140 | end 141 | config_template[2].items = items 142 | config_template[2].value = items[1] 143 | end 144 | 145 | -- Macro execution 146 | local function load_macro() 147 | -- Show UI 148 | local ok, config = aegisub.dialog.display(config_template, {"Calculate"}) 149 | -- OK from UI 150 | if ok then 151 | -- Save UI configuration to template 152 | local config_template_n, config_template_entry = #config_template 153 | for config_key, config_value in pairs(config) do 154 | for i=1, config_template_n do 155 | config_template_entry = config_template[i] 156 | if config_template_entry.name == config_key then 157 | if config_template_entry.value then 158 | config_template_entry.value = config_value 159 | elseif config_template_entry.text then 160 | config_template_entry.text = config_value 161 | end 162 | break 163 | end 164 | end 165 | end 166 | -- Calculate shape from configuration settings 167 | local text_shape = Yutils.decode.create_font(config.fontname, config.bold, config.italic, config.underline, config.strikeout, config.fontsize, config.scale_x / 100, config.scale_y / 100, config.spacing).text_to_shape(config.text) 168 | if (config.wobble_frequency_x > 0 and config.wobble_strength_x > 0) or (config.wobble_frequency_y > 0 and config.wobble_strength_y > 0) then 169 | text_shape = Yutils.shape.filter( 170 | Yutils.shape.split( 171 | Yutils.shape.flatten( 172 | text_shape 173 | ), 174 | 1 175 | ), 176 | function(x,y) 177 | return x + math.sin(y * config.wobble_frequency_x * math.pi * 2) * config.wobble_strength_x, 178 | y + math.sin(x * config.wobble_frequency_y * math.pi * 2) * config.wobble_strength_y 179 | end 180 | ) 181 | end 182 | -- Show calculated shape 183 | aegisub.log(text_shape) 184 | end 185 | end 186 | 187 | -- Register macro to Aegisub 188 | aegisub.register_macro(script_name,script_description,load_macro) -------------------------------------------------------------------------------- /docs/content_format.js: -------------------------------------------------------------------------------- 1 | // Prepend child to element 2 | Element.prototype.prependChild = function(child){ 3 | this.insertBefore(child, this.firstChild); 4 | } 5 | 6 | // Get computed final style of element 7 | Element.prototype.getStyle = function(){ 8 | return this.currentStyle || window.getComputedStyle(this); 9 | } 10 | 11 | // Get width of browser scrollbar 12 | function getScrollBarWidth(){ 13 | // Barwidth not already known? 14 | if(!getScrollBarWidth.prototype.barWidth){ 15 | // Create outer+inner box 16 | var outerBox = document.createElement("div"), 17 | innerBox = document.createElement("div"); 18 | // Set box widths 19 | outerBox.style.width = "100px"; 20 | innerBox.style.width = "100%"; 21 | // Set outer box invisible (no effect on displayed elements) 22 | outerBox.style.visibility = "hidden"; 23 | outerBox.style.position = "fixed"; 24 | outerBox.style.left = 0; 25 | outerBox.style.top = 0; 26 | // Link boxes to parent elements (for data generation) 27 | outerBox.appendChild(innerBox); 28 | document.body.appendChild(outerBox); 29 | // Save scrollbar width 30 | outerBox.style.overflow = "scroll"; 31 | var widthWithScrollBar = innerBox.offsetWidth; 32 | outerBox.style.overflow = "hidden"; 33 | getScrollBarWidth.prototype.barWidth = innerBox.offsetWidth - widthWithScrollBar + "px"; 34 | // Remove boxes (no further need) 35 | document.body.removeChild(outerBox); 36 | } 37 | // Return saved barwidth 38 | return getScrollBarWidth.prototype.barWidth; 39 | } 40 | 41 | // Add element padding/space for scrollbar to not cover content 42 | function fixScrollBar(elem, direction){ 43 | if((!direction || direction == "vertical") && elem.clientHeight < elem.scrollHeight){ 44 | elem.style.paddingRight = getScrollBarWidth(); 45 | elem.style.overflowX = "hidden"; 46 | } 47 | if((!direction || direction == "horizontal") && elem.clientWidth < elem.scrollWidth){ 48 | elem.style.paddingBottom = getScrollBarWidth(); 49 | elem.style.overflowY = "hidden"; 50 | } 51 | } 52 | 53 | // Execute on page load finished 54 | window.addEventListener("load", function(evt){ 55 | // Process contents table and sections 56 | var contents = document.getElementsByClassName("contents")[0]; 57 | sections = document.getElementsByClassName("section"); 58 | for(var i = 0; i < sections.length; ++i){ 59 | var section = sections[i]; 60 | // Add link to section in contents table 61 | var link = document.createElement("a"); 62 | link.href = "#" + section.id; 63 | link.appendChild(document.createTextNode(section.id)); 64 | link.className = "contents"; 65 | contents.appendChild(link); 66 | // Add anchor to section 67 | var anchor = document.createElement("a"); 68 | anchor.name = section.id; 69 | section.prependChild(anchor); 70 | // Set section title (text on top of section box) 71 | var title = document.createElement("span"); 72 | title.appendChild(document.createTextNode(section.id)); 73 | title.id = "section"; 74 | section.parentNode.insertBefore(title, section); 75 | // Repeat last steps with subsections / functions 76 | var functions = section.getElementsByClassName("function"), 77 | lastLibrary = ""; 78 | for(var j=0; j < functions.length; ++j){ 79 | section = functions[j]; 80 | // Extract function definition 81 | if(section.firstChild){ 82 | var funcDef = section.firstChild.textContent; 83 | // Break function in chunks 84 | var assign = funcDef.indexOf("="), 85 | bracketOpen = funcDef.indexOf("("), 86 | bracketClose = funcDef.indexOf(")"); 87 | if(bracketOpen != -1 && bracketClose != -1 && bracketOpen < bracketClose && bracketOpen > 0){ 88 | var funcRet = assign != -1 ? funcDef.slice(0,assign).trim() : "", 89 | funcName = funcDef.slice(assign != -1 ? assign+1 : 0,bracketOpen).trim(), 90 | funcParam = funcDef.slice(bracketOpen+1,bracketClose).trim(); 91 | // Add link to function in contents table 92 | link = document.createElement("a"); 93 | link.href = "#" + funcName; 94 | link.appendChild(document.createTextNode(funcName)); 95 | link.className = "subcontents"; 96 | contents.appendChild(link); 97 | // Add function indention & library header to contents 98 | var libSeparator = funcName.indexOf("."); 99 | if(libSeparator != -1){ 100 | var libName = funcName.slice(0, libSeparator); 101 | if(libName == libName.toUpperCase()) 102 | link.style.paddingLeft = parseInt(link.getStyle().paddingLeft) * 3 + "px"; 103 | else{ 104 | if(lastLibrary != libName){ 105 | var libLink = link.cloneNode(true); 106 | libLink.replaceChild(document.createTextNode(libName), libLink.firstChild); 107 | contents.insertBefore(libLink, link); 108 | lastLibrary = libName; 109 | } 110 | link.style.paddingLeft = parseInt(link.getStyle().paddingLeft) * 2 + "px"; 111 | } 112 | }else 113 | lastLibrary = ""; 114 | // Add anchor to section 115 | anchor = document.createElement("a"); 116 | anchor.name = funcName; 117 | section.prependChild(anchor); 118 | // Style function definition 119 | var defStyle = document.createElement("span"); 120 | if(funcRet.length > 0){ 121 | var color = document.createElement("span"); 122 | color.className = "function_return"; 123 | color.appendChild(document.createTextNode(funcRet)); 124 | defStyle.appendChild(color); 125 | defStyle.appendChild(document.createTextNode(" = ")); 126 | } 127 | defStyle.appendChild(document.createTextNode(funcName + "(")); 128 | var color = document.createElement("span"); 129 | color.className = "function_parameters"; 130 | color.appendChild(document.createTextNode(funcParam)); 131 | defStyle.appendChild(color); 132 | defStyle.appendChild(document.createTextNode(")")); 133 | defStyle.className = "definition"; 134 | section.replaceChild(defStyle,section.childNodes[1]); 135 | } 136 | } 137 | } 138 | } 139 | fixScrollBar(contents, "vertical"); 140 | // Process code chunks 141 | var codes = document.getElementsByClassName("code"); 142 | for(var i = 0; i < codes.length; ++i){ 143 | var code = codes[i]; 144 | if(code.tagName.toLowerCase() == "div" && code.lastChild){ 145 | // Add linenumber cell to code 146 | var numberBar = document.createElement("td"), 147 | numberText = "1"; 148 | for(var j=2; j<=code.lastChild.textContent.split("\n").length; ++j) 149 | numberText += "\n" + j; 150 | numberBar.appendChild(document.createTextNode(numberText)); 151 | numberBar.className = "linenumbers"; 152 | code.appendChild(numberBar); 153 | // Put code in own cell 154 | var codeText = document.createElement("td"); 155 | codeText.appendChild(code.firstChild.cloneNode()); 156 | code.replaceChild(codeText,code.firstChild); 157 | // Transfer padding from code box to cells 158 | var codeStyle = code.getStyle(); 159 | numberBar.style.paddingLeft = codeStyle.paddingLeft, 160 | numberBar.style.paddingRight = codeStyle.paddingRight, 161 | numberBar.style.paddingTop = codeStyle.paddingTop, 162 | numberBar.style.paddingBottom = codeStyle.paddingBottom, 163 | codeText.style.paddingLeft = codeStyle.paddingLeft, 164 | codeText.style.paddingRight = codeStyle.paddingRight, 165 | codeText.style.paddingTop = codeStyle.paddingTop, 166 | codeText.style.paddingBottom = codeStyle.paddingBottom, 167 | code.style.padding = 0; 168 | // Add right space for vertical scrollbar (on appearance) 169 | fixScrollBar(code, "vertical"); 170 | // Set code highlight 171 | code.addEventListener("mouseover", function(evt){ 172 | evt.currentTarget.childNodes[1].style.display = "table-cell"; 173 | }) 174 | code.addEventListener("mouseout", function(evt){ 175 | evt.currentTarget.childNodes[1].style.display = "none"; 176 | }) 177 | } 178 | } 179 | }) -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yutils 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 |

YutilsAn ASS typeset utilities library

26 |
27 |
28 |
29 | Yutils is a Lua library with functions for media decoding, shape manipulation, advanced math, utf-8 coded texts, ASS (Advanced Substation Alpha) script parsing and with some other small helpers.
30 | She was written to run with LuaJIT, working with Lua 5.2 syntax and FFI.
31 |
32 |
33 | Yutils runs without (additional) dependencies with LuaJIT 2 as interpreter, though some functionality might require to install a library. So far, just some decodings, like fonts and PNG images, could lead you to some handwork.
34 | The base is just one Lua script, not more. Include and use it in your project, no frills beside.
35 |
36 | What you can extend: 37 | 41 |
42 | How Yutils work can be influenced by changes in her configuration which is a few local variables at the code beginning (directly after the first comment lines).
43 | For example, lowering the value of CURVE_TOLERANCE could cause to smoother edges on curve flattening, same time resulting in more output lines and slower calculation. The default values are already well considered in weight of enough usefulness and perfection insanity, so be careful when changing anything (values don't get checked, so you could break things). 44 |
45 |
46 | Yutils consists of functions only, packed in sublibrary tables, those packed in one library table which you get from library file loading.
47 |
local Yutils = require("Yutils") 48 | local Ymath = Yutils.math 49 | 50 | print(Ymath.distance(1,1)) 51 | -- 1.4142135623731

52 | It's also possible to load+execute the library script with the first argument as true to register Yutils to the global scope.
53 |
loadfile("Yutils.lua")(true) 54 | local Ymath = Yutils.math
55 |
56 |
57 |
58 | new_table = table.copy(t[, depth])
59 | Copies elements (recursively) of table t into returned table new_table.
60 | depth can limit the number of recursive passes, so value 1 results in a shallow copy. 61 |
62 |
63 | table_description = table.tostring(t)
64 | Converts table t into readable string table_description (mainly useful for debugging). 65 |
66 |
67 | range = utf8.charrange(s, i)
68 | Returns byte range of unicode character in string s at position i. 69 |
70 |
71 | chars_iter = utf8.chars(s)
72 | Creates iterator function through unicode characters of string s.
73 | Every iteration pass returns character index + string or nil on end. 74 |
75 |
76 | length = utf8.len(s)
77 | Returns unicode characters number of string s. 78 |
79 |
80 | c0x0, c0y0, c0x1, c0y1, c0x2, c0y2, c0x3, c0y3[, c1x0, c1y0, c1x1, c1y1, c1x2, c1y2, c1x3, c1y3[, c2x0, c2y0, c2x1, c2y1, c2x2, c2y2, c2x3, c2y3[, c3x0, c3y0, c3x1, c3y1, c3x2, c3y2, c3x3, c3y3]]] = math.arc_curve(x, y, cx, cy, angle)
81 | Converts arc data to bezier curves.
82 | x & y is the arc starting point, cx & cy the arc center (= orientation point to keep the same distance to all arc points) and angle the angle in degree of the arc.
83 | For each 90° one curve is generated, so a maximum of 4 curves can span a circle. Curves are 3rd order bezier curves, defined as c<CURVE_INDEX>x|y<POINT_INDEX>. 84 |
85 |
86 | x, y, z = math.bezier(pct, pts)
87 | Calculates a point on a bezier curve of any order.
88 | pct is the position on the curve in range 0<=x<=1. pts is a table of tables, each one containing 2 or 3 numbers as curve point. 89 |
90 |
91 | MATRIX = math.create_matrix()
92 | Creates a 3D matrix object (for usage, see following functions). 93 |
94 |
95 | matrix_fields = MATRIX.get_data()
96 | Returns a table with 16 numbers, presenting all matrix fields.
97 | 98 | 99 | 100 | 101 | 102 |
15913
261014
371115
481216
103 | 104 | 105 | 106 | 107 | 108 |
x0x1y0x1z0x1w0x1
x0y1y0y1z0y1w0y1
x0z1y0z1z0z1w0z1
x0w1y0w1z0w1w0w1
109 |
110 |
111 | MATRIX = MATRIX.set_data(matrix_fields)
112 | Sets matrix fields. For more, see MATRIX.get_data. 113 |
114 |
115 | MATRIX = MATRIX.identity()
116 | Resets matrix to identity. 117 |
118 |
119 | MATRIX = MATRIX.multiply(matrix_fields)
120 | Multiplies another matrix as table/raw data to this matrix. This way, matrix properties get prepended. For more, see MATRIX.get_data. 121 |
122 |
123 | MATRIX = MATRIX.translate(x, y, z)
124 | Applies a translation to the matrix. 125 |
126 |
127 | MATRIX = MATRIX.scale(x, y, z)
128 | Applies a scale to the matrix. 129 |
130 |
131 | MATRIX = MATRIX.rotate(axis, angle)
132 | Applies a rotation to the matrix. axis can be "x", "y" or "z", angle is in degree. 133 |
134 |
135 | [MATRIX] = MATRIX.inverse()
136 | Inverses the matrix. This might fail, so nothing will be returned. 137 |
138 |
139 | rx, ry, rz, rw = MATRIX.transform(x, y, z[, w])
140 | Multiplies a point with the matrix, returning a new one with all properties of the matrix added. 141 |
142 |
143 | degree = math.degree(x1, y1, z1, x2, y2, z2)
144 | Calculates the degree between vectors x1|y1|z1 and x2|y2|z3. 145 |
146 |
147 | length = math.distance(x, y[, z])
148 | Calculates length of given vector. 149 |
150 |
151 | x, y = math.line_intersect(x0, y0, x1, y1, x2, y2, x3, y3, strict)
152 | Calculates intersection point of two lines.
153 | x0, y0, x1, y1 are both points of line 1, x2, y2, x3, y3 are points of line 2. strict is a flag, determining the intersection has to be located on the lines.
154 | x, y can be the intersection point. If both lines are parallel, x is nil. If strict is true and there's no intersection on the strict length lines, x is inf (1/0). 155 |
156 |
157 | rx, ry, rz = math.ortho(x1, y1, z1, x2, y2, z2)
158 | Calculates the orthogonal vector to vectors x1|y1|z1 and x2|y2|z3. 159 |
160 |
161 | r = math.randomsteps(min, max, step)
162 | Generates randomly a number in range min to max with gap size step between numbers. 163 |
164 |
165 | r = math.round(x[, dec])
166 | Rounds x to nearest integer. Optionally, dec defines the position behind decimal point to round to. 167 |
168 |
169 | rx, ry, rz = math.stretch(x, y, z, length)
170 | Stretches vector x|y|z to length length. 171 |
172 |
173 | r = math.trim(x, min, max)
174 | If x is smaller than min, returns min. If x is greater than max, returns max. Otherwise returns x. 175 |
176 |
177 | frames_iter = algorithm.frames(starts, ends, dur)
178 | Creates iterator function for frames in range starts to ends with step size dur.
179 | For each frame, the start, end, index and number of all frames becomes available. 180 |
181 |
182 | lines_iter = algorithm.lines(text)
183 | Creates iterator function through lines of text text.
184 | All 3 sorts of line endings (CR, LF, CRLF) will be considered. 185 |
186 |
187 | x0, y0, x1, y1 = shape.bounding(shape)
188 | Calculates the bounding box of shape shape.
189 | x0|y0 is the upper-left and x1|y1 the lower-right corner of the rectangle. 190 |
191 |
192 | shapes = shape.detect(width, height, data[, compare_func])
193 | Traces shapes in 2D data.
194 | width and height defines vector lengths => how to read elements in data table data.
195 | compare_func can be defined as comparison function, useful for non-flat data elements.
196 | Each entry in returned table shapes contains following fields: 197 | 201 |
202 |
203 | new_shape = shape.filter(shape, filter)
204 | Filters points of shape shape by function filter and returns a new one.
205 | filter receives point coordinates x and y as well as the point type and have to return 2 numbers, replacing x and y. 206 |
207 |
208 | flattened_shape = shape.flatten(shape)
209 | Converts all 3rd order bezier curves in shape shape to lines, creating a new shape. 210 |
211 |
212 | new_shape = shape.glue(src_shape, dst_shape[, transform_callback])
213 | Projects shape src_shape with his bottom on the first figure of shape dst_shape, returned as new shape. src_shape gets stretched to fit on dst_shape.
214 | transform_callback can be defined as callback function, receiving the position on dst_shape in range 0<=x<=1 and the orthogonal offset, having to return the replacement. 215 |
216 |
217 | new_shape = shape.move(shape, x, y)
218 | Shifts points of shape shape horizontally by x and vertically by y, creating a new shape. 219 |
220 |
221 | new_shape = shape.split(shape, max_len)
222 | Splits lines of shape shape into shorter ones to fix to maximal line length of max_len, creating a new shape. 223 |
224 |
225 | outline_shape = shape.to_outline(shape, width_xy[, width_y][, mode])
226 | Converts shape shape from his filling to the stroke with horizontal width width_xy, vertical width width_y and join type mode which can be "round"|"bevel"|"miter", returned as new shape. If width_y isn't defined, width_xy stands for both. Default join type is "round". 227 |
228 |
229 | pixels = shape.to_pixels(shape)
230 | Renders shape shape and returns pixels.
231 | pixels is a table of single pixels, each one with following fields: 232 | 237 |
238 |
239 | transformed_shape = shape.transform(shape, MATRIX)
240 | Applies a matrix (see math.create_matrix) on shape points of shape, creating a new shape. 241 |
242 |
243 | ms_ass = ass.convert_time(ass_ms)
244 | Converts time between numeric and ASS presentation.
245 | ass_ms can be a string in ASS format H:MM:SS.XX (H=Hours, M=Minutes, S=Seconds, X=Milliseconds*10) or milliseconds as number.
246 | ms_ass becomes the equivalent of ass_ms. 247 |
248 |
249 | a_r_ass[, rg, rb[, ra]] = ass.convert_coloralpha(ass_r_a[, g, b[, a]])
250 | Converts color, alpha or color+alpha between numeric and ASS presentation.
251 | ass_r_a can be a string as ASS color (&HFFFFFF&), alpha (&HFF&) or both (&HFFFFFFFF) as well as the color strength of red or alpha in range 0<=x<=255. g is green, b is blue, a is alpha, all color strengths in same range.
252 | a_r_ass[, rg, rb[, ra]] becomes the equivalent of ass_r_a[, g, b[, a]]. 253 |
254 |
255 | coloralpha = ass.interpolate_coloralpha(pct, ...)
256 | Interpolates between multiple ASS color, alpha or color+alpha ... and calculates the value at position pct with range 0<=x<=1. 257 |
258 |
259 | PARSER = ass.create_parser([ass_text])
260 | Creates an ASS parser object (for usage, see following functions). ass_text can be an ASS script/text to be parsed. 261 |
262 |
263 | accepted = PARSER.parse_line(line)
264 | Parses text line line to add ASS data to the parser object.
265 | If the line was valid and data added, accepted is true, otherwise false. 266 |
267 |
268 | meta = PARSER.meta()
269 | Returns ASS meta data as table with following fields: 270 | 276 |
277 |
278 | styles = PARSER.styles()
279 | Returns ASS styles as table. Table keys are style names, values are tables with following fields: 280 | 300 |
301 |
302 | dialogs = PARSER.dialogs([extended])
303 | Returns ASS dialogs as table. Each entry is a table with following fields: 304 | 317 | If extended is true, following additional fields are added: 318 | 418 | Some additional informations to calculated values:
419 |
420 |
421 |
422 | BMP_READER = decode.create_bmp_reader(filename)
423 | Creates a bitmap reader object (for usage, see following functions). Decodes Windows Bitmap (or PNG) file filename. 424 |
425 |
426 | file_size = BMP_READER.file_size()
427 | Returns bitmap file size in bytes. 428 |
429 |
430 | width = BMP_READER.width()
431 | Returns bitmap width. 432 |
433 |
434 | height = BMP_READER.height()
435 | Returns bitmap height. 436 |
437 |
438 | bit_depth = BMP_READER.bit_depth()
439 | Returns bitmap bit depth. 440 |
441 |
442 | data_size = BMP_READER.data_size()
443 | Returns bitmap image data size in bytes. 444 |
445 |
446 | row_size = BMP_READER.row_size()
447 | Returns bitmap image data row size in bytes. 448 |
449 |
450 | is_bottom_up = BMP_READER.bottom_up()
451 | Returns whether bitmap rows are to read bottom-up (instead top-down). 452 |
453 |
454 | data_raw = BMP_READER.data_raw()
455 | Returns bitmap image data bytes as string. 456 |
457 |
458 | data_packed = BMP_READER.data_packed()
459 | Returns bitmap image data packed in a table. Each entry is a pixel table with following fields: 460 | 466 |
467 |
468 | data_ass_text = BMP_READER.data_text()
469 | Returns bitmap image data as ASS text. This text is optimized for small length. 470 |
471 |
472 | WAV_READER = decode.create_wav_reader(filename)
473 | Creates an audio wave reader object (for usage, see following functions). Decodes wave file filename. 474 |
475 |
476 | file_size = WAV_READER.file_size()
477 | Returns wave file size in bytes. 478 |
479 |
480 | channels = WAV_READER.channels_number()
481 | Returns number of audio channels. 482 |
483 |
484 | sample_rate = WAV_READER.sample_rate()
485 | Returns audio sample rate / samples per second per channel. 486 |
487 |
488 | byte_rate = WAV_READER.byte_rate()
489 | Returns wave byte rate / bytes per second. 490 |
491 |
492 | block_align = WAV_READER.block_align()
493 | Returns wave data block size in bytes (byte depth * number of channels). 494 |
495 |
496 | bits_per_sample = WAV_READER.bits_per_sample()
497 | Returns audio sample bit depth / byte size (/8) of one sample. 498 |
499 |
500 | samples_number = WAV_READER.samples_per_channel()
501 | Returns audio samples number per channel. 502 |
503 |
504 | min_amplitude, max_amplitude = WAV_READER.min_max_amplitude()
505 | Returns minimal and miximal possible value of audio samples. 506 |
507 |
508 | sample = WAV_READER.sample_from_ms(ms)
509 | Returns sample index at time ms in milliseconds. 510 |
511 |
512 | ms = WAV_READER.ms_from_sample(sample)
513 | Returns time in milliseconds at sample index sample. 514 |
515 |
516 | position = WAV_READER.position([pos])
517 | Sets and returns current position (= sample index) of samples stream. 518 |
519 |
520 | samples = WAV_READER.samples_interlaced(n)
521 | Reads n samples from wave data stream and returns them as entries of table samples.
522 | Samples get read raw, so still interlaced. 523 |
524 |
525 | samples = WAV_READER.samples(n)
526 | Like WAV_READER.samples_interlaced, but packs samples in subtables for every channel. 527 |
528 |
529 | FREQ_ANALYZER = decode.create_frequency_analyzer(samples, sample_rate)
530 | Creates a frequency analyzer object (for usage, see following functions). Analyzes frequencies of audio samples samples with sample rate sample_rate with FFT. 531 |
532 |
533 | frequencies = FREQ_ANALYZER.frequencies()
534 | Returns frequencies in range 0<=x<=sample_rate/2 as table. Each entry has following fields: 535 | 539 |
540 |
541 | weight = FREQ_ANALYZER.frequency_weight(freq)
542 | Returns weight of frequency freq in sum of all. If this frequency isn't available, interpolates the value from the neighbours. 543 |
544 |
545 | weight = FREQ_ANALYZER.frequency_range_weight(freq_min, freq_max)
546 | Calculates weight of frequencies in range freq_min to freq_max. 547 |
548 |
549 | FONT_HANDLE = decode.create_font(family, bold, italic, underline, strikeout, size[, xscale][, yscale][, hspace])
550 | Creates a font object (for usage, see following functions).
551 | family is the font family.
552 | If bold is true, font has bold weight.
553 | If italic is true, font has italic style.
554 | If underline is true, font has underline decoration.
555 | If strikeout is true, font has strikeout decoration.
556 | size is the font size.
557 | xscale and yscale can define horizontal & vertical scale.
558 | hspace can define intercharacter space. 559 |
560 |
561 | metrics = FONT_HANDLE.metrics()
562 | Returns font metrics as table with followings fields: 563 | 570 |
571 |
572 | extents = FONT_HANDLE.text_extents(text)
573 | Returns extents of text with given font as table with followings fields: 574 | 578 |
579 |
580 | shape = FONT_HANDLE.text_to_shape(text)
581 | Converts text with given font to an ASS shape. 582 |
583 |
584 | fonts_list = decode.list_fonts([with_filenames])
585 | Returns a list of system installed fonts.
586 | fonts_list is a table, each entry one font. Fonts contain following fields: 587 | 594 |
595 |
596 |
597 | Example scripts how Yutils functions can be used you'll find in folder tests. 598 |
599 | 600 | 601 | -------------------------------------------------------------------------------- /src/Yutils.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2014, Christoph "Youka" Spanknebel 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | ----------------------------------------------------------------------------------------------------------------- 22 | Version: 17th January 2015, 15:45 (GMT+1) 23 | 24 | Yutils 25 | table 26 | copy(t[, depth]) -> table 27 | tostring(t) -> string 28 | utf8 29 | charrange(s, i) -> number 30 | chars(s) -> function 31 | len(s) -> number 32 | math 33 | arc_curve(x, y, cx, cy, angle) -> 8, 16, 24 or 32 numbers 34 | bezier(pct, pts) -> number, number, number 35 | create_matrix() -> table 36 | get_data() -> table 37 | set_data(matrix) -> table 38 | identity() -> table 39 | multiply(matrix2) -> table 40 | translate(x, y, z) -> table 41 | scale(x, y, z) -> table 42 | rotate(axis, angle) -> table 43 | inverse() -> [table] 44 | transform(x, y, z[, w]) -> number, number, number, number 45 | degree(x1, y1, z1, x2, y2, z2) -> number 46 | distance(x, y[, z]) -> number 47 | line_intersect(x0, y0, x1, y1, x2, y2, x3, y3, strict) -> number, number|nil|inf 48 | ortho(x1, y1, z1, x2, y2, z2) -> number, number, number 49 | randomsteps(min, max, step) -> number 50 | round(x[, dec]) -> number 51 | stretch(x, y, z, length) -> number, number, number 52 | trim(x, min, max) -> number 53 | algorithm 54 | frames(starts, ends, dur) -> function 55 | lines(text) -> function 56 | shape 57 | bounding(shape) -> number, number, number, number 58 | detect(width, height, data[, compare_func]) -> table 59 | filter(shape, filter) -> string 60 | flatten(shape) -> string 61 | glue(src_shape, dst_shape[, transform_callback]) -> string 62 | move(shape, x, y) -> string 63 | split(shape, max_len) -> string 64 | to_outline(shape, width_xy[, width_y][, mode]) -> string 65 | to_pixels(shape) -> table 66 | transform(shape, matrix) -> string 67 | ass 68 | convert_time(ass_ms) -> number|string 69 | convert_coloralpha(ass_r_a[, g, b[, a] ]) -> 1,3,4 numbers|string 70 | interpolate_coloralpha(pct, ...) -> string 71 | create_parser([ass_text]) -> table 72 | parse_line(line) -> boolean 73 | meta() -> table 74 | styles() -> table 75 | dialogs([extended]) -> table 76 | decode 77 | create_bmp_reader(filename) -> table 78 | file_size() -> number 79 | width() -> number 80 | height() -> number 81 | bit_depth() -> number 82 | data_size() -> number 83 | row_size() -> number 84 | bottom_up() -> boolean 85 | data_raw() -> string 86 | data_packed() -> table 87 | data_text() -> string 88 | create_wav_reader(filename) -> table 89 | file_size() -> number 90 | channels_number() -> number 91 | sample_rate() -> number 92 | byte_rate() -> number 93 | block_align() -> number 94 | bits_per_sample() -> number 95 | samples_per_channel() -> number 96 | min_max_amplitude() -> number, number 97 | sample_from_ms(ms) -> number 98 | ms_from_sample(sample) -> number 99 | position([pos]) -> number 100 | samples_interlaced(n) -> table 101 | samples(n) -> table 102 | create_frequency_analyzer(samples, sample_rate) -> table 103 | frequencies() -> table 104 | frequency_weight(freq) -> number 105 | frequency_range_weight(freq_min, freq_max) -> number 106 | create_font(family, bold, italic, underline, strikeout, size[, xscale][, yscale][, hspace]) -> table 107 | metrics() -> table 108 | text_extents(text) -> table 109 | text_to_shape(text) -> string 110 | list_fonts([with_filenames]) -> table 111 | ]] 112 | 113 | -- Configuration 114 | local FP_PRECISION = 3 -- Floating point precision by numbers behind point (for shape points) 115 | local CURVE_TOLERANCE = 1 -- Angle in degree to define a curve as flat 116 | local MAX_CIRCUMFERENCE = 1.5 -- Circumference step size to create round edges out of lines 117 | local MITER_LIMIT = 200 -- Maximal length of a miter join 118 | local SUPERSAMPLING = 8 -- Anti-aliasing precision for shape to pixels conversion 119 | local FONT_PRECISION = 64 -- Font scale for better precision output from native font system 120 | local LIBASS_FONTHACK = true -- Scale font data to fontsize? (no effect on windows) 121 | local LIBPNG_PATH = "libpng" -- libpng dynamic library location or shortcut (for system library loading function) 122 | 123 | -- Load FFI interface 124 | local ffi = require("ffi") 125 | -- Check OS & load fitting system libraries 126 | local advapi, pangocairo, fontconfig 127 | if ffi.os == "Windows" then 128 | -- WinGDI already loaded in C namespace by default 129 | -- Load advanced winapi library 130 | advapi = ffi.load("Advapi32") 131 | -- Set C definitions for WinAPI 132 | ffi.cdef([[ 133 | enum{CP_UTF8 = 65001}; 134 | enum{MM_TEXT = 1}; 135 | enum{TRANSPARENT = 1}; 136 | enum{ 137 | FW_NORMAL = 400, 138 | FW_BOLD = 700 139 | }; 140 | enum{DEFAULT_CHARSET = 1}; 141 | enum{OUT_TT_PRECIS = 4}; 142 | enum{CLIP_DEFAULT_PRECIS = 0}; 143 | enum{ANTIALIASED_QUALITY = 4}; 144 | enum{DEFAULT_PITCH = 0x0}; 145 | enum{FF_DONTCARE = 0x0}; 146 | enum{ 147 | PT_MOVETO = 0x6, 148 | PT_LINETO = 0x2, 149 | PT_BEZIERTO = 0x4, 150 | PT_CLOSEFIGURE = 0x1 151 | }; 152 | typedef unsigned int UINT; 153 | typedef unsigned long DWORD; 154 | typedef DWORD* LPDWORD; 155 | typedef const char* LPCSTR; 156 | typedef const wchar_t* LPCWSTR; 157 | typedef wchar_t* LPWSTR; 158 | typedef char* LPSTR; 159 | typedef void* HANDLE; 160 | typedef HANDLE HDC; 161 | typedef int BOOL; 162 | typedef BOOL* LPBOOL; 163 | typedef unsigned int size_t; 164 | typedef HANDLE HFONT; 165 | typedef HANDLE HGDIOBJ; 166 | typedef long LONG; 167 | typedef wchar_t WCHAR; 168 | typedef unsigned char BYTE; 169 | typedef BYTE* LPBYTE; 170 | typedef int INT; 171 | typedef long LPARAM; 172 | static const int LF_FACESIZE = 32; 173 | static const int LF_FULLFACESIZE = 64; 174 | typedef struct{ 175 | LONG tmHeight; 176 | LONG tmAscent; 177 | LONG tmDescent; 178 | LONG tmInternalLeading; 179 | LONG tmExternalLeading; 180 | LONG tmAveCharWidth; 181 | LONG tmMaxCharWidth; 182 | LONG tmWeight; 183 | LONG tmOverhang; 184 | LONG tmDigitizedAspectX; 185 | LONG tmDigitizedAspectY; 186 | WCHAR tmFirstChar; 187 | WCHAR tmLastChar; 188 | WCHAR tmDefaultChar; 189 | WCHAR tmBreakChar; 190 | BYTE tmItalic; 191 | BYTE tmUnderlined; 192 | BYTE tmStruckOut; 193 | BYTE tmPitchAndFamily; 194 | BYTE tmCharSet; 195 | }TEXTMETRICW, *LPTEXTMETRICW; 196 | typedef struct{ 197 | LONG cx; 198 | LONG cy; 199 | }SIZE, *LPSIZE; 200 | typedef struct{ 201 | LONG left; 202 | LONG top; 203 | LONG right; 204 | LONG bottom; 205 | }RECT; 206 | typedef const RECT* LPCRECT; 207 | typedef struct{ 208 | LONG x; 209 | LONG y; 210 | }POINT, *LPPOINT; 211 | typedef struct{ 212 | LONG lfHeight; 213 | LONG lfWidth; 214 | LONG lfEscapement; 215 | LONG lfOrientation; 216 | LONG lfWeight; 217 | BYTE lfItalic; 218 | BYTE lfUnderline; 219 | BYTE lfStrikeOut; 220 | BYTE lfCharSet; 221 | BYTE lfOutPrecision; 222 | BYTE lfClipPrecision; 223 | BYTE lfQuality; 224 | BYTE lfPitchAndFamily; 225 | WCHAR lfFaceName[LF_FACESIZE]; 226 | }LOGFONTW, *LPLOGFONTW; 227 | typedef struct{ 228 | LOGFONTW elfLogFont; 229 | WCHAR elfFullName[LF_FULLFACESIZE]; 230 | WCHAR elfStyle[LF_FACESIZE]; 231 | WCHAR elfScript[LF_FACESIZE]; 232 | }ENUMLOGFONTEXW, *LPENUMLOGFONTEXW; 233 | enum{ 234 | FONTTYPE_RASTER = 1, 235 | FONTTYPE_DEVICE = 2, 236 | FONTTYPE_TRUETYPE = 4 237 | }; 238 | typedef int (__stdcall *FONTENUMPROC)(const ENUMLOGFONTEXW*, const void*, DWORD, LPARAM); 239 | enum{ERROR_SUCCESS = 0}; 240 | typedef HANDLE HKEY; 241 | typedef HKEY* PHKEY; 242 | enum{HKEY_LOCAL_MACHINE = 0x80000002}; 243 | typedef enum{KEY_READ = 0x20019}REGSAM; 244 | 245 | int MultiByteToWideChar(UINT, DWORD, LPCSTR, int, LPWSTR, int); 246 | int WideCharToMultiByte(UINT, DWORD, LPCWSTR, int, LPSTR, int, LPCSTR, LPBOOL); 247 | HDC CreateCompatibleDC(HDC); 248 | BOOL DeleteDC(HDC); 249 | int SetMapMode(HDC, int); 250 | int SetBkMode(HDC, int); 251 | size_t wcslen(const wchar_t*); 252 | HFONT CreateFontW(int, int, int, int, int, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, LPCWSTR); 253 | HGDIOBJ SelectObject(HDC, HGDIOBJ); 254 | BOOL DeleteObject(HGDIOBJ); 255 | BOOL GetTextMetricsW(HDC, LPTEXTMETRICW); 256 | BOOL GetTextExtentPoint32W(HDC, LPCWSTR, int, LPSIZE); 257 | BOOL BeginPath(HDC); 258 | BOOL ExtTextOutW(HDC, int, int, UINT, LPCRECT, LPCWSTR, UINT, const INT*); 259 | BOOL EndPath(HDC); 260 | int GetPath(HDC, LPPOINT, LPBYTE, int); 261 | BOOL AbortPath(HDC); 262 | int EnumFontFamiliesExW(HDC, LPLOGFONTW, FONTENUMPROC, LPARAM, DWORD); 263 | LONG RegOpenKeyExA(HKEY, LPCSTR, DWORD, REGSAM, PHKEY); 264 | LONG RegCloseKey(HKEY); 265 | LONG RegEnumValueW(HKEY, DWORD, LPWSTR, LPDWORD, LPDWORD, LPDWORD, LPBYTE, LPDWORD); 266 | ]]) 267 | else -- Unix 268 | -- Attempt to load pangocairo library 269 | pcall(function() 270 | pangocairo = ffi.load("pangocairo-1.0.so") -- Extension must be appended because of dot already in filename 271 | -- Set C definitions for pangocairo 272 | ffi.cdef([[ 273 | typedef enum{ 274 | CAIRO_FORMAT_INVALID = -1, 275 | CAIRO_FORMAT_ARGB32 = 0, 276 | CAIRO_FORMAT_RGB24 = 1, 277 | CAIRO_FORMAT_A8 = 2, 278 | CAIRO_FORMAT_A1 = 3, 279 | CAIRO_FORMAT_RGB16_565 = 4, 280 | CAIRO_FORMAT_RGB30 = 5 281 | }cairo_format_t; 282 | typedef void cairo_surface_t; 283 | typedef void cairo_t; 284 | typedef void PangoLayout; 285 | typedef void* gpointer; 286 | static const int PANGO_SCALE = 1024; 287 | typedef void PangoFontDescription; 288 | typedef enum{ 289 | PANGO_WEIGHT_THIN = 100, 290 | PANGO_WEIGHT_ULTRALIGHT = 200, 291 | PANGO_WEIGHT_LIGHT = 300, 292 | PANGO_WEIGHT_NORMAL = 400, 293 | PANGO_WEIGHT_MEDIUM = 500, 294 | PANGO_WEIGHT_SEMIBOLD = 600, 295 | PANGO_WEIGHT_BOLD = 700, 296 | PANGO_WEIGHT_ULTRABOLD = 800, 297 | PANGO_WEIGHT_HEAVY = 900, 298 | PANGO_WEIGHT_ULTRAHEAVY = 1000 299 | }PangoWeight; 300 | typedef enum{ 301 | PANGO_STYLE_NORMAL, 302 | PANGO_STYLE_OBLIQUE, 303 | PANGO_STYLE_ITALIC 304 | }PangoStyle; 305 | typedef void PangoAttrList; 306 | typedef void PangoAttribute; 307 | typedef enum{ 308 | PANGO_UNDERLINE_NONE, 309 | PANGO_UNDERLINE_SINGLE, 310 | PANGO_UNDERLINE_DOUBLE, 311 | PANGO_UNDERLINE_LOW, 312 | PANGO_UNDERLINE_ERROR 313 | }PangoUnderline; 314 | typedef int gint; 315 | typedef gint gboolean; 316 | typedef void PangoContext; 317 | typedef unsigned int guint; 318 | typedef struct{ 319 | guint ref_count; 320 | int ascent; 321 | int descent; 322 | int approximate_char_width; 323 | int approximate_digit_width; 324 | int underline_position; 325 | int underline_thickness; 326 | int strikethrough_position; 327 | int strikethrough_thickness; 328 | }PangoFontMetrics; 329 | typedef void PangoLanguage; 330 | typedef struct{ 331 | int x; 332 | int y; 333 | int width; 334 | int height; 335 | }PangoRectangle; 336 | typedef enum{ 337 | CAIRO_STATUS_SUCCESS = 0 338 | }cairo_status_t; 339 | typedef enum{ 340 | CAIRO_PATH_MOVE_TO, 341 | CAIRO_PATH_LINE_TO, 342 | CAIRO_PATH_CURVE_TO, 343 | CAIRO_PATH_CLOSE_PATH 344 | }cairo_path_data_type_t; 345 | typedef union{ 346 | struct{ 347 | cairo_path_data_type_t type; 348 | int length; 349 | }header; 350 | struct{ 351 | double x, y; 352 | }point; 353 | }cairo_path_data_t; 354 | typedef struct{ 355 | cairo_status_t status; 356 | cairo_path_data_t* data; 357 | int num_data; 358 | }cairo_path_t; 359 | 360 | cairo_surface_t* cairo_image_surface_create(cairo_format_t, int, int); 361 | void cairo_surface_destroy(cairo_surface_t*); 362 | cairo_t* cairo_create(cairo_surface_t*); 363 | void cairo_destroy(cairo_t*); 364 | PangoLayout* pango_cairo_create_layout(cairo_t*); 365 | void g_object_unref(gpointer); 366 | PangoFontDescription* pango_font_description_new(void); 367 | void pango_font_description_free(PangoFontDescription*); 368 | void pango_font_description_set_family(PangoFontDescription*, const char*); 369 | void pango_font_description_set_weight(PangoFontDescription*, PangoWeight); 370 | void pango_font_description_set_style(PangoFontDescription*, PangoStyle); 371 | void pango_font_description_set_absolute_size(PangoFontDescription*, double); 372 | void pango_layout_set_font_description(PangoLayout*, PangoFontDescription*); 373 | PangoAttrList* pango_attr_list_new(void); 374 | void pango_attr_list_unref(PangoAttrList*); 375 | void pango_attr_list_insert(PangoAttrList*, PangoAttribute*); 376 | PangoAttribute* pango_attr_underline_new(PangoUnderline); 377 | PangoAttribute* pango_attr_strikethrough_new(gboolean); 378 | PangoAttribute* pango_attr_letter_spacing_new(int); 379 | void pango_layout_set_attributes(PangoLayout*, PangoAttrList*); 380 | PangoContext* pango_layout_get_context(PangoLayout*); 381 | const PangoFontDescription* pango_layout_get_font_description(PangoLayout*); 382 | PangoFontMetrics* pango_context_get_metrics(PangoContext*, const PangoFontDescription*, PangoLanguage*); 383 | void pango_font_metrics_unref(PangoFontMetrics*); 384 | int pango_font_metrics_get_ascent(PangoFontMetrics*); 385 | int pango_font_metrics_get_descent(PangoFontMetrics*); 386 | int pango_layout_get_spacing(PangoLayout*); 387 | void pango_layout_set_text(PangoLayout*, const char*, int); 388 | void pango_layout_get_pixel_extents(PangoLayout*, PangoRectangle*, PangoRectangle*); 389 | void cairo_save(cairo_t*); 390 | void cairo_restore(cairo_t*); 391 | void cairo_scale(cairo_t*, double, double); 392 | void pango_cairo_layout_path(cairo_t*, PangoLayout*); 393 | void cairo_new_path(cairo_t*); 394 | cairo_path_t* cairo_copy_path(cairo_t*); 395 | void cairo_path_destroy(cairo_path_t*); 396 | ]]) 397 | end) 398 | -- Attempt to load fontconfig library 399 | pcall(function() 400 | fontconfig = ffi.load("fontconfig") 401 | -- Set C definitions for fontconfig 402 | ffi.cdef([[ 403 | typedef void FcConfig; 404 | typedef void FcPattern; 405 | typedef struct{ 406 | int nobject; 407 | int sobject; 408 | const char** objects; 409 | }FcObjectSet; 410 | typedef struct{ 411 | int nfont; 412 | int sfont; 413 | FcPattern** fonts; 414 | }FcFontSet; 415 | typedef enum{ 416 | FcResultMatch, 417 | FcResultNoMatch, 418 | FcResultTypeMismatch, 419 | FcResultNoId, 420 | FcResultOutOfMemory 421 | }FcResult; 422 | typedef unsigned char FcChar8; 423 | typedef int FcBool; 424 | 425 | FcConfig* FcInitLoadConfigAndFonts(void); 426 | FcPattern* FcPatternCreate(void); 427 | void FcPatternDestroy(FcPattern*); 428 | FcObjectSet* FcObjectSetBuild(const char*, ...); 429 | void FcObjectSetDestroy(FcObjectSet*); 430 | FcFontSet* FcFontList(FcConfig*, FcPattern*, FcObjectSet*); 431 | void FcFontSetDestroy(FcFontSet*); 432 | FcResult FcPatternGetString(FcPattern*, const char*, int, FcChar8**); 433 | FcResult FcPatternGetBool(FcPattern*, const char*, int, FcBool*); 434 | ]]) 435 | end) 436 | end 437 | -- Load PNG decode library (at least try it) 438 | local libpng 439 | pcall(function() 440 | libpng = ffi.load(LIBPNG_PATH) 441 | -- Set C definitions for libpng 442 | ffi.cdef([[ 443 | static const int PNG_SIGNATURE_SIZE = 8; 444 | typedef unsigned char png_byte; 445 | typedef png_byte* png_bytep; 446 | typedef const png_bytep png_const_bytep; 447 | typedef unsigned int png_size_t; 448 | typedef char png_char; 449 | typedef png_char* png_charp; 450 | typedef const png_charp png_const_charp; 451 | typedef void png_void; 452 | typedef png_void* png_voidp; 453 | typedef struct png_struct* png_structp; 454 | typedef const png_structp png_const_structp; 455 | typedef struct png_info* png_infop; 456 | typedef const png_infop png_const_infop; 457 | typedef unsigned int png_uint_32; 458 | typedef void (__cdecl *png_error_ptr)(png_structp, png_const_charp); 459 | typedef void (__cdecl *png_rw_ptr)(png_structp, png_bytep, png_size_t); 460 | enum{ 461 | PNG_TRANSFORM_STRIP_16 = 0x1, 462 | PNG_TRANSFORM_PACKING = 0x4, 463 | PNG_TRANSFORM_EXPAND = 0x10, 464 | PNG_TRANSFORM_BGR = 0x80 465 | }; 466 | enum{ 467 | PNG_COLOR_MASK_COLOR = 2, 468 | PNG_COLOR_MASK_ALPHA = 4 469 | }; 470 | enum{ 471 | PNG_COLOR_TYPE_RGB = PNG_COLOR_MASK_COLOR, 472 | PNG_COLOR_TYPE_RGBA = PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_ALPHA 473 | }; 474 | 475 | void* memcpy(void*, const void*, size_t); 476 | int png_sig_cmp(png_const_bytep, png_size_t, png_size_t); 477 | png_structp png_create_read_struct(png_const_charp, png_voidp, png_error_ptr, png_error_ptr); 478 | void png_destroy_read_struct(png_structp*, png_infop*, png_infop*); 479 | png_infop png_create_info_struct(png_structp); 480 | void png_set_read_fn(png_structp, png_voidp, png_rw_ptr); 481 | void png_read_png(png_structp, png_infop, int, png_voidp); 482 | int png_set_interlace_handling(png_structp); 483 | void png_read_update_info(png_structp, png_infop); 484 | png_uint_32 png_get_image_width(png_const_structp, png_const_infop); 485 | png_uint_32 png_get_image_height(png_const_structp, png_const_infop); 486 | png_byte png_get_color_type(png_const_structp, png_const_infop); 487 | png_size_t png_get_rowbytes(png_const_structp, png_const_infop); 488 | png_bytep* png_get_rows(png_const_structp, png_const_infop); 489 | ]]) 490 | end) 491 | 492 | -- Helper functions 493 | local unpack = table.unpack or unpack 494 | local function rotate2d(x, y, angle) 495 | local ra = math.rad(angle) 496 | return math.cos(ra)*x - math.sin(ra)*y, 497 | math.sin(ra)*x + math.cos(ra)*y 498 | end 499 | local function bton(s) 500 | -- Get numeric presentation (=byte) of string characters 501 | local bytes, n = {s:byte(1,-1)}, 0 502 | -- Combine bytes to unsigned integer number 503 | for i = 0, #s-1 do 504 | n = n + bytes[1+i] * 256^i 505 | end 506 | return n 507 | end 508 | local function utf8_to_utf16(s) 509 | -- Get resulting utf16 characters number (+ null-termination) 510 | local wlen = ffi.C.MultiByteToWideChar(ffi.C.CP_UTF8, 0x0, s, -1, nil, 0) 511 | -- Allocate array for utf16 characters storage 512 | local ws = ffi.new("wchar_t[?]", wlen) 513 | -- Convert utf8 string to utf16 characters 514 | ffi.C.MultiByteToWideChar(ffi.C.CP_UTF8, 0x0, s, -1, ws, wlen) 515 | -- Return utf16 C string 516 | return ws 517 | end 518 | local function utf16_to_utf8(ws) 519 | -- Get resulting utf8 characters number (+ null-termination) 520 | local slen = ffi.C.WideCharToMultiByte(ffi.C.CP_UTF8, 0x0, ws, -1, nil, 0, nil, nil) 521 | -- Allocate array for utf8 characters storage 522 | local s = ffi.new("char[?]", slen) 523 | -- Convert utf16 string to utf8 characters 524 | ffi.C.WideCharToMultiByte(ffi.C.CP_UTF8, 0x0, ws, -1, s, slen, nil, nil) 525 | -- Return utf8 Lua string 526 | return ffi.string(s) 527 | end 528 | 529 | -- Create library table 530 | local Yutils 531 | Yutils = { 532 | -- Table sublibrary 533 | table = { 534 | -- Copies table deep 535 | copy = function(t, depth) 536 | -- Check argument 537 | if type(t) ~= "table" or depth ~= nil and not(type(depth) == "number" and depth >= 1) then 538 | error("table and optional depth expected", 2) 539 | end 540 | -- Copy & return 541 | local function copy_recursive(old_t) 542 | local new_t = {} 543 | for key, value in pairs(old_t) do 544 | new_t[key] = type(value) == "table" and copy_recursive(value) or value 545 | end 546 | return new_t 547 | end 548 | local function copy_recursive_n(old_t, depth) 549 | local new_t = {} 550 | for key, value in pairs(old_t) do 551 | new_t[key] = type(value) == "table" and depth >= 2 and copy_recursive_n(value, depth-1) or value 552 | end 553 | return new_t 554 | end 555 | return depth and copy_recursive_n(t, depth) or copy_recursive(t) 556 | end, 557 | -- Converts table to string 558 | tostring = function(t) 559 | -- Check argument 560 | if type(t) ~= "table" then 561 | error("table expected", 2) 562 | end 563 | -- Result storage 564 | local result, result_n = {}, 0 565 | -- Convert to string! 566 | local function convert_recursive(t, space) 567 | for key, value in pairs(t) do 568 | if type(key) == "string" then 569 | key = string.format("%q", key) 570 | end 571 | if type(value) == "string" then 572 | value = string.format("%q", value) 573 | end 574 | result_n = result_n + 1 575 | result[result_n] = string.format("%s[%s] = %s", space, key, value) 576 | if type(value) == "table" then 577 | convert_recursive(value, space .. "\t") 578 | end 579 | end 580 | end 581 | convert_recursive(t, "") 582 | -- Return result as string 583 | return table.concat(result, "\n") 584 | end 585 | }, 586 | -- UTF8 sublibrary 587 | utf8 = { 588 | --[[ 589 | UTF32 -> UTF8 590 | -------------- 591 | U-00000000 - …U-0000007F: 0xxxxxxx 592 | U-00000080 - U-000007FF: 110xxxxx 10xxxxxx 593 | U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx 594 | U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 595 | U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 596 | U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 597 | ]] 598 | -- UTF8 character range at string codepoint 599 | charrange = function(s, i) 600 | -- Check arguments 601 | if type(s) ~= "string" or type(i) ~= "number" or i < 1 or i > #s then 602 | error("string and string index expected", 2) 603 | end 604 | -- Evaluate codepoint to range 605 | local byte = s:byte(i) 606 | return not byte and 0 or 607 | byte < 192 and 1 or 608 | byte < 224 and 2 or 609 | byte < 240 and 3 or 610 | byte < 248 and 4 or 611 | byte < 252 and 5 or 612 | 6 613 | end, 614 | -- Creates iterator through UTF8 characters 615 | chars = function(s) 616 | -- Check argument 617 | if type(s) ~= "string" then 618 | error("string expected", 2) 619 | end 620 | -- Return utf8 characters iterator 621 | local char_i, s_pos, s_len = 0, 1, #s 622 | return function() 623 | if s_pos <= s_len then 624 | local cur_pos = s_pos 625 | s_pos = s_pos + Yutils.utf8.charrange(s, s_pos) 626 | if s_pos-1 <= s_len then 627 | char_i = char_i + 1 628 | return char_i, s:sub(cur_pos, s_pos-1) 629 | end 630 | end 631 | end 632 | end, 633 | -- Get UTF8 characters number in string 634 | len = function(s) 635 | -- Check argument 636 | if type(s) ~= "string" then 637 | error("string expected", 2) 638 | end 639 | -- Count UTF8 characters 640 | local n = 0 641 | for _ in Yutils.utf8.chars(s) do 642 | n = n + 1 643 | end 644 | return n 645 | end 646 | }, 647 | -- Math sublibrary 648 | math = { 649 | -- Converts an arc to 1-4 cubic bezier curve(s) 650 | arc_curve = function(x, y, cx, cy, angle) 651 | -- Check arguments 652 | if type(x) ~= "number" or type(y) ~= "number" or type(cx) ~= "number" or type(cy) ~= "number" or type(angle) ~= "number" or 653 | angle < -360 or angle > 360 then 654 | error("start & center point and valid angle (-360<=x<=360) expected", 2) 655 | end 656 | -- Something to do? 657 | if angle ~= 0 then 658 | -- Factor for bezier control points distance to node points 659 | local kappa = 4 * (math.sqrt(2) - 1) / 3 660 | -- Relative points to center 661 | local rx0, ry0, rx1, ry1, rx2, ry2, rx3, ry3, rx03, ry03 = x - cx, y - cy 662 | -- Define arc clock direction & set angle to positive range 663 | local cw = angle > 0 and 1 or -1 664 | if angle < 0 then 665 | angle = -angle 666 | end 667 | -- Create curves in 90 degree chunks 668 | local curves, curves_n, angle_sum, cur_angle_pct = {}, 0, 0 669 | repeat 670 | -- Get arc end point 671 | cur_angle_pct = math.min(angle - angle_sum, 90) / 90 672 | rx3, ry3 = rotate2d(rx0, ry0, cw * 90 * cur_angle_pct) 673 | -- Get arc start to end vector 674 | rx03, ry03 = rx3 - rx0, ry3 - ry0 675 | -- Scale arc vector to curve node <-> control point distance 676 | rx03, ry03 = Yutils.math.stretch(rx03, ry03, 0, math.sqrt(Yutils.math.distance(rx03, ry03)^2/2) * kappa) 677 | -- Get curve control points 678 | rx1, ry1 = rotate2d(rx03, ry03, cw * -45 * cur_angle_pct) 679 | rx1, ry1 = rx0 + rx1, ry0 + ry1 680 | rx2, ry2 = rotate2d(-rx03, -ry03, cw * 45 * cur_angle_pct) 681 | rx2, ry2 = rx3 + rx2, ry3 + ry2 682 | -- Insert curve to output 683 | curves[curves_n+1], curves[curves_n+2], curves[curves_n+3], curves[curves_n+4], 684 | curves[curves_n+5], curves[curves_n+6], curves[curves_n+7], curves[curves_n+8] = 685 | cx + rx0, cy + ry0, cx + rx1, cy + ry1, cx + rx2, cy + ry2, cx + rx3, cy + ry3 686 | curves_n = curves_n + 8 687 | -- Prepare next curve 688 | rx0, ry0 = rx3, ry3 689 | angle_sum = angle_sum + 90 690 | until angle_sum >= angle 691 | -- Return curve points as tuple 692 | return unpack(curves) 693 | end 694 | end, 695 | -- Get point on n-degree bezier curve 696 | bezier = function(pct, pts) 697 | -- Check arguments 698 | if type(pct) ~= "number" or pct < 0 or pct > 1 or type(pts) ~= "table" then 699 | error("percent number and points table expected", 2) 700 | end 701 | local pts_n = #pts 702 | if pts_n < 2 then 703 | error("at least 2 points expected", 2) 704 | end 705 | for _, value in ipairs(pts) do 706 | if type(value[1]) ~= "number" or type(value[2]) ~= "number" or (value[3] ~= nil and type(value[3]) ~= "number") then 707 | error("points have to be tables with 2 or 3 numbers", 2) 708 | end 709 | end 710 | -- Pick a fitting fast calculation 711 | local pct_inv = 1 - pct 712 | if pts_n == 2 then -- Linear curve 713 | return pct_inv * pts[1][1] + pct * pts[2][1], 714 | pct_inv * pts[1][2] + pct * pts[2][2], 715 | pts[1][3] and pts[2][3] and pct_inv * pts[1][3] + pct * pts[2][3] or 0 716 | elseif pts_n == 3 then -- Quadratic curve 717 | return pct_inv * pct_inv * pts[1][1] + 2 * pct_inv * pct * pts[2][1] + pct * pct * pts[3][1], 718 | pct_inv * pct_inv * pts[1][2] + 2 * pct_inv * pct * pts[2][2] + pct * pct * pts[3][2], 719 | 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 720 | elseif pts_n == 4 then -- Cubic curve 721 | 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], 722 | 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], 723 | 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 724 | else -- pts_n > 4 725 | -- Factorial 726 | local function fac(n) 727 | local k = 1 728 | for i=2, n do 729 | k = k * i 730 | end 731 | return k 732 | end 733 | -- Calculate coordinate 734 | local ret_x, ret_y, ret_z = 0, 0, 0 735 | local n, bern, pt = pts_n - 1 736 | for i=0, n do 737 | pt = pts[1+i] 738 | -- Bernstein polynom 739 | bern = fac(n) / (fac(i) * fac(n - i)) * --Binomial coefficient 740 | pct^i * pct_inv^(n - i) 741 | ret_x = ret_x + pt[1] * bern 742 | ret_y = ret_y + pt[2] * bern 743 | ret_z = ret_z + (pt[3] or 0) * bern 744 | end 745 | return ret_x, ret_y, ret_z 746 | end 747 | end, 748 | -- Creates 3d matrix 749 | create_matrix = function() 750 | -- Matrix data 751 | local matrix = {1, 0, 0, 0, 752 | 0, 1, 0, 0, 753 | 0, 0, 1, 0, 754 | 0, 0, 0, 1} 755 | -- Matrix object 756 | local obj 757 | obj = { 758 | -- Get matrix data 759 | get_data = function() 760 | return Yutils.table.copy(matrix) 761 | end, 762 | -- Set matrix data 763 | set_data = function(new_matrix) 764 | -- Check arguments 765 | if type(new_matrix) ~= "table" or #new_matrix ~= 16 then 766 | error("4x4 matrix expected", 2) 767 | end 768 | for _, value in ipairs(new_matrix) do 769 | if type(value) ~= "number" then 770 | error("matrix must contain only numbers", 2) 771 | end 772 | end 773 | -- Replace old matrix 774 | matrix = Yutils.table.copy(new_matrix) 775 | -- Return this object 776 | return obj 777 | end, 778 | -- Set matrix to identity 779 | identity = function() 780 | -- Set matrix to default / no transformation 781 | matrix[1] = 1 782 | matrix[2] = 0 783 | matrix[3] = 0 784 | matrix[4] = 0 785 | matrix[5] = 0 786 | matrix[6] = 1 787 | matrix[7] = 0 788 | matrix[8] = 0 789 | matrix[9] = 0 790 | matrix[10] = 0 791 | matrix[11] = 1 792 | matrix[12] = 0 793 | matrix[13] = 0 794 | matrix[14] = 0 795 | matrix[15] = 0 796 | matrix[16] = 1 797 | -- Return this object 798 | return obj 799 | end, 800 | -- Multiplies matrix with given one 801 | multiply = function(matrix2) 802 | -- Check arguments 803 | if type(matrix2) ~= "table" or #matrix2 ~= 16 then 804 | error("4x4 matrix expected", 2) 805 | end 806 | for _, value in ipairs(matrix2) do 807 | if type(value) ~= "number" then 808 | error("matrix must contain only numbers", 2) 809 | end 810 | end 811 | -- Multipy matrices to create new one 812 | local new_matrix = {0, 0, 0, 0, 813 | 0, 0, 0, 0, 814 | 0, 0, 0, 0, 815 | 0, 0, 0, 0} 816 | for i=1, 16 do 817 | for j=0, 3 do 818 | new_matrix[i] = new_matrix[i] + matrix[1 + (i-1) % 4 + j * 4] * matrix2[1 + math.floor((i-1) / 4) * 4 + j] 819 | end 820 | end 821 | -- Replace old matrix with multiply result 822 | matrix = new_matrix 823 | -- Return this object 824 | return obj 825 | end, 826 | -- Applies translation to matrix 827 | translate = function(x, y, z) 828 | -- Check arguments 829 | if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then 830 | error("3 translation values expected", 2) 831 | end 832 | -- Add translation to matrix 833 | obj.multiply({1, 0, 0, 0, 834 | 0, 1, 0, 0, 835 | 0, 0, 1, 0, 836 | x, y, z, 1}) 837 | -- Return this object 838 | return obj 839 | end, 840 | -- Applies scale to matrix 841 | scale = function(x, y, z) 842 | -- Check arguments 843 | if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" then 844 | error("3 scale factors expected", 2) 845 | end 846 | -- Add scale to matrix 847 | obj.multiply({x, 0, 0, 0, 848 | 0, y, 0, 0, 849 | 0, 0, z, 0, 850 | 0, 0, 0, 1}) 851 | -- Return this object 852 | return obj 853 | end, 854 | -- Applies rotation to matrix 855 | rotate = function(axis, angle) 856 | -- Check arguments 857 | if (axis ~= "x" and axis ~= "y" and axis ~= "z") or type(angle) ~= "number" then 858 | error("axis (as string) and angle (in degree) expected", 2) 859 | end 860 | -- Convert angle from degree to radian 861 | angle = math.rad(angle) 862 | -- Rotate by axis 863 | if axis == "x" then 864 | obj.multiply({1, 0, 0, 0, 865 | 0, math.cos(angle), -math.sin(angle), 0, 866 | 0, math.sin(angle), math.cos(angle), 0, 867 | 0, 0, 0, 1}) 868 | elseif axis == "y" then 869 | obj.multiply({math.cos(angle), 0, math.sin(angle), 0, 870 | 0, 1, 0, 0, 871 | -math.sin(angle), 0, math.cos(angle), 0, 872 | 0, 0, 0, 1}) 873 | else -- axis == "z" 874 | obj.multiply({math.cos(angle), -math.sin(angle), 0, 0, 875 | math.sin(angle), math.cos(angle), 0, 0, 876 | 0, 0, 1, 0, 877 | 0, 0, 0, 1}) 878 | end 879 | -- Return this object 880 | return obj 881 | end, 882 | -- Inverses matrix 883 | inverse = function() 884 | -- Create inversion matrix 885 | local inv_matrix = { 886 | 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], 887 | -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], 888 | 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], 889 | -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], 890 | -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], 891 | 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], 892 | -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], 893 | 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], 894 | 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], 895 | -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], 896 | 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], 897 | -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], 898 | -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], 899 | 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], 900 | -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], 901 | 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] 902 | } 903 | -- Calculate determinant 904 | local det = matrix[1] * inv_matrix[1] + 905 | matrix[5] * inv_matrix[2] + 906 | matrix[9] * inv_matrix[3] + 907 | matrix[13] * inv_matrix[4] 908 | -- Matrix inversion possible? 909 | if det ~= 0 then 910 | -- Invert matrix 911 | det = 1 / det 912 | for i=1, 16 do 913 | matrix[i] = inv_matrix[i] * det 914 | end 915 | -- Return this object 916 | return obj 917 | end 918 | end, 919 | -- Applies matrix to point 920 | transform = function(x, y, z, w) 921 | -- Check arguments 922 | if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" or (w ~= nil and type(w) ~= "number") then 923 | error("point (3 or 4 numbers) expected", 2) 924 | end 925 | -- Set 4th coordinate 926 | if not w then 927 | w = 1 928 | end 929 | -- Calculate new point 930 | return x * matrix[1] + y * matrix[5] + z * matrix[9] + w * matrix[13], 931 | x * matrix[2] + y * matrix[6] + z * matrix[10] + w * matrix[14], 932 | x * matrix[3] + y * matrix[7] + z * matrix[11] + w * matrix[15], 933 | x * matrix[4] + y * matrix[8] + z * matrix[12] + w * matrix[16] 934 | end 935 | } 936 | return obj 937 | end, 938 | -- Degree between two 3d vectors 939 | degree = function(x1, y1, z1, x2, y2, z2) 940 | -- Check arguments 941 | if type(x1) ~= "number" or type(y1) ~= "number" or type(z1) ~= "number" or 942 | type(x2) ~= "number" or type(y2) ~= "number" or type(z2) ~= "number" then 943 | error("2 vectors (as 6 numbers) expected", 2) 944 | end 945 | -- Calculate degree 946 | local degree = math.deg( 947 | math.acos( 948 | (x1 * x2 + y1 * y2 + z1 * z2) / 949 | (Yutils.math.distance(x1, y1, z1) * Yutils.math.distance(x2, y2, z2)) 950 | ) 951 | ) 952 | -- Return with sign by clockwise direction 953 | return (x1*y2 - y1*x2) < 0 and -degree or degree 954 | end, 955 | -- Length of vector 956 | distance = function(x, y, z) 957 | -- Check arguments 958 | if type(x) ~= "number" or type(y) ~= "number" or z ~= nil and type(z) ~= "number" then 959 | error("one vector (2 or 3 numbers) expected", 2) 960 | end 961 | -- Calculate length 962 | return z and math.sqrt(x*x + y*y + z*z) or math.sqrt(x*x + y*y) 963 | end, 964 | line_intersect = function(x0, y0, x1, y1, x2, y2, x3, y3, strict) 965 | -- Check arguments 966 | if type(x0) ~= "number" or type(y0) ~= "number" or type(x1) ~= "number" or type(y1) ~= "number" or 967 | type(x2) ~= "number" or type(y2) ~= "number" or type(x3) ~= "number" or type(y3) ~= "number" or 968 | strict ~= nil and type(strict) ~= "boolean" then 969 | error("two lines and optional strictness flag expected", 2) 970 | end 971 | -- Get line vectors & check valid lengths 972 | local x10, y10, x32, y32 = x0 - x1, y0 - y1, x2 - x3, y2 - y3 973 | if x10 == 0 and y10 == 0 or x32 == 0 and y32 == 0 then 974 | error("lines mustn't have zero length", 2) 975 | end 976 | -- Calculate determinant and check for parallel lines 977 | local det = x10 * y32 - y10 * x32 978 | if det ~= 0 then 979 | -- Calculate line intersection (endless line lengths) 980 | local pre, post = (x0 * y1 - y0 * x1), (x2 * y3 - y2 * x3) 981 | local ix, iy = (pre * x32 - x10 * post) / det, (pre * y32 - y10 * post) / det 982 | -- Check for line intersection with given line lengths 983 | if strict then 984 | local s, t = x10 ~= 0 and (ix - x1) / x10 or (iy - y1) / y10, x32 ~= 0 and (ix - x3) / x32 or (iy - y3) / y32 985 | if s < 0 or s > 1 or t < 0 or t > 1 then 986 | return 1/0 -- inf 987 | end 988 | end 989 | -- Return intersection point 990 | return ix, iy 991 | end 992 | end, 993 | -- Get orthogonal vector of 2 given vectors 994 | ortho = function(x1, y1, z1, x2, y2, z2) 995 | -- Check arguments 996 | if type(x1) ~= "number" or type(y1) ~= "number" or type(z1) ~= "number" or 997 | type(x2) ~= "number" or type(y2) ~= "number" or type(z2) ~= "number" then 998 | error("2 vectors (as 6 numbers) expected", 2) 999 | end 1000 | -- Calculate orthogonal 1001 | return y1 * z2 - z1 * y2, 1002 | z1 * x2 - x1 * z2, 1003 | x1 * y2 - y1 * x2 1004 | end, 1005 | -- Generates a random number in given range with specific item distance 1006 | randomsteps = function(min, max, step) 1007 | -- Check arguments 1008 | if type(min) ~= "number" or type(max) ~= "number" or type(step) ~= "number" or max < min or step <= 0 then 1009 | error("minimal, maximal and step number expected", 2) 1010 | end 1011 | -- Generate random number 1012 | return math.min(min + math.random(0, math.ceil((max - min) / step)) * step, max) 1013 | end, 1014 | -- Rounds number 1015 | round = function(x, dec) 1016 | -- Check argument 1017 | if type(x) ~= "number" or dec ~= nil and type(dec) ~= "number" then 1018 | error("number and optional number expected", 2) 1019 | end 1020 | -- Return number rounded to wished decimal size 1021 | if dec and dec >= 1 then 1022 | dec = 10^math.floor(dec) 1023 | return math.floor(x * dec + 0.5) / dec 1024 | else 1025 | return math.floor(x + 0.5) 1026 | end 1027 | end, 1028 | -- Scales vector to given length 1029 | stretch = function(x, y, z, length) 1030 | -- Check arguments 1031 | if type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" or type(length) ~= "number" then 1032 | error("vector (3d) and length expected", 2) 1033 | end 1034 | -- Get current vector length 1035 | local cur_length = Yutils.math.distance(x, y, z) 1036 | -- Scale vector to new length 1037 | if cur_length == 0 then 1038 | return 0, 0, 0 1039 | else 1040 | local factor = length / cur_length 1041 | return x * factor, y * factor, z * factor 1042 | end 1043 | end, 1044 | -- Trim number in range 1045 | trim = function(x, min, max) 1046 | -- Check arguments 1047 | if type(x) ~= "number" or type(min) ~= "number" or type(max) ~= "number" then 1048 | error("3 numbers expected", 2) 1049 | end 1050 | -- Limit number bigger-equal minimal value and smaller-equal maximal value 1051 | return x < min and min or x > max and max or x 1052 | end 1053 | }, 1054 | -- Algorithm sublibrary 1055 | algorithm = { 1056 | -- Creates iterator through frame times 1057 | frames = function(starts, ends, dur) 1058 | -- Check arguments 1059 | if type(starts) ~= "number" or type(ends) ~= "number" or type(dur) ~= "number" or dur == 0 then 1060 | error("start, end and duration number expected", 2) 1061 | end 1062 | -- Iteration state 1063 | local i, n = 0, math.ceil((ends - starts) / dur) 1064 | -- Return iterator 1065 | return function() 1066 | i = i + 1 1067 | if i <= n then 1068 | local ret_starts = starts + (i-1) * dur 1069 | local ret_ends = ret_starts + dur 1070 | if dur < 0 and ret_ends < ends or dur > 0 and ret_ends > ends then 1071 | ret_ends = ends 1072 | end 1073 | return ret_starts, ret_ends, i, n 1074 | end 1075 | end 1076 | end, 1077 | -- Creates iterator through text lines 1078 | lines = function(text) 1079 | -- Check argument 1080 | if type(text) ~= "string" then 1081 | error("string expected", 2) 1082 | end 1083 | -- Return iterator 1084 | return function() 1085 | -- Still text left? 1086 | if text then 1087 | -- Find possible line endings 1088 | local cr = text:find("\r", 1, true) 1089 | local lf = text:find("\n", 1, true) 1090 | -- Find earliest line ending 1091 | local text_end, next_step = #text, 0 1092 | if lf then 1093 | text_end, next_step = lf-1, 2 1094 | end 1095 | if cr then 1096 | if not lf or cr < lf-1 then 1097 | text_end, next_step = cr-1, 2 1098 | elseif cr == lf-1 then 1099 | text_end, next_step = cr-1, 3 1100 | end 1101 | end 1102 | -- Cut line out & update text 1103 | local line = text:sub(1, text_end) 1104 | if next_step == 0 then 1105 | text = nil 1106 | else 1107 | text = text:sub(text_end+next_step) 1108 | end 1109 | -- Return current line 1110 | return line 1111 | end 1112 | end 1113 | end 1114 | }, 1115 | -- Shape sublibrary 1116 | shape = { 1117 | -- Calculates shape bounding box 1118 | bounding = function(shape) 1119 | -- Check argument 1120 | if type(shape) ~= "string" then 1121 | error("shape expected", 2) 1122 | end 1123 | -- Bounding data 1124 | local x0, y0, x1, y1 1125 | -- Calculate minimal and maximal coordinates 1126 | Yutils.shape.filter(shape, function(x, y) 1127 | if x0 then 1128 | x0, y0, x1, y1 = math.min(x0, x), math.min(y0, y), math.max(x1, x), math.max(y1, y) 1129 | else 1130 | x0, y0, x1, y1 = x, y, x, y 1131 | end 1132 | end) 1133 | return x0, y0, x1, y1 1134 | end, 1135 | -- Extracts shapes by similar data in 2d data map 1136 | detect = function(width, height, data, compare_func) 1137 | -- Check arguments 1138 | 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 1139 | error("width, height, data and optional data compare function expected", 2) 1140 | end 1141 | -- Set default comparator 1142 | if not compare_func then 1143 | compare_func = function(a, b) return a == b end 1144 | end 1145 | -- Maximal data number to be processed 1146 | local data_n = width * height 1147 | -- Collect unique data elements 1148 | local elements = {n = 1, {value = data[1]}} 1149 | for i=2, data_n do 1150 | for j=1, elements.n do 1151 | if compare_func(data[i], elements[j].value) then 1152 | goto trace_element_found 1153 | end 1154 | end 1155 | elements.n = elements.n + 1 1156 | elements[elements.n] = {value = type(data[i]) == "table" and Yutils.table.copy(data[i]) or data[i]} 1157 | ::trace_element_found:: 1158 | end 1159 | -- Detection helper functions 1160 | local function index_to_x(i) 1161 | return (i-1) % width 1162 | end 1163 | local function index_to_y(i) 1164 | return math.floor((i-1) / width) 1165 | end 1166 | local function coord_to_index(x, y) 1167 | return 1 + x + y * width 1168 | end 1169 | local function find_direction(bitmap, x, y, last_direction) 1170 | local top_left, top_right, bottom_left, bottom_right = 1171 | x-1 >= 0 and y-1 >= 0 and bitmap[coord_to_index(x-1,y-1)] == 1 or false, 1172 | x < width and y-1 >= 0 and bitmap[coord_to_index(x,y-1)] == 1 or false, 1173 | x-1 >= 0 and y < height and bitmap[coord_to_index(x-1,y)] == 1 or false, 1174 | x < width and y < height and bitmap[coord_to_index(x,y)] == 1 or false 1175 | return last_direction == 8 and ( 1176 | bottom_left and ( 1177 | top_left and top_right and 6 or 1178 | top_left and 8 or 1179 | 4 1180 | ) or ( -- bottom_right 1181 | top_left and top_right and 4 or 1182 | top_right and 8 or 1183 | 6 1184 | ) 1185 | ) or last_direction == 6 and ( 1186 | top_left and ( 1187 | top_right and bottom_right and 2 or 1188 | top_right and 6 or 1189 | 8 1190 | )or ( -- bottom_left 1191 | top_right and bottom_right and 8 or 1192 | bottom_right and 6 or 1193 | 2 1194 | ) 1195 | ) or last_direction == 2 and ( 1196 | top_left and ( 1197 | bottom_left and bottom_right and 6 or 1198 | bottom_left and 2 or 1199 | 4 1200 | ) or ( -- top_right 1201 | bottom_left and bottom_right and 4 or 1202 | bottom_right and 2 or 1203 | 6 1204 | ) 1205 | ) or last_direction == 4 and ( 1206 | top_right and ( 1207 | top_left and bottom_left and 2 or 1208 | top_left and 4 or 1209 | 8 1210 | ) or ( -- bottom_right 1211 | top_left and bottom_left and 8 or 1212 | bottom_left and 4 or 1213 | 2 1214 | ) 1215 | ) 1216 | end 1217 | local function extract_contour(bitmap, x, y, cw) 1218 | 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}} 1219 | repeat 1220 | x, y = contour[contour.n].x2, contour[contour.n].y2 1221 | direction = find_direction(bitmap, x, y, contour[contour.n].direction) 1222 | contour.n = contour.n + 1 1223 | 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} 1224 | until contour[contour.n].x2 == contour[1].x1 and contour[contour.n].y2 == contour[1].y1 1225 | return contour 1226 | end 1227 | local function contour_indices(contour) 1228 | -- Get top & bottom line of contour 1229 | local min_y, max_y, line 1230 | for i=1, contour.n do 1231 | line = contour[i] 1232 | if line.direction == 8 then 1233 | 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 1234 | elseif line.direction == 2 then 1235 | 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 1236 | end 1237 | end 1238 | -- Get indices by scanlines 1239 | local indices, h_stops, h_stops_n, j = {n = 0} 1240 | for y=min_y, max_y do 1241 | h_stops, h_stops_n = {}, 0 1242 | for i=1, contour.n do 1243 | line = contour[i] 1244 | if line.direction == 8 and line.y2 == y or line.direction == 2 and line.y1 == y then 1245 | h_stops_n = h_stops_n + 1 1246 | h_stops[h_stops_n] = line.x1 1247 | end 1248 | end 1249 | table.sort(h_stops) 1250 | for i=1, h_stops_n, 2 do 1251 | j = coord_to_index(h_stops[i], y) 1252 | for x_off=0, h_stops[i+1] - h_stops[i] - 1 do 1253 | indices.n = indices.n + 1 1254 | indices[indices.n] = j + x_off 1255 | end 1256 | end 1257 | end 1258 | return indices 1259 | end 1260 | local function merge_contour_lines(contour) 1261 | local i = 1 1262 | while i < contour.n do 1263 | if contour[i].direction == contour[i+1].direction then 1264 | contour[i].x2, contour[i].y2 = contour[i+1].x2, contour[i+1].y2 1265 | table.remove(contour, i+1) 1266 | contour.n = contour.n - 1 1267 | else 1268 | i = i + 1 1269 | end 1270 | end 1271 | if contour.n > 1 and contour[1].direction == contour[contour.n].direction then 1272 | contour[1].x1, contour[1].y1 = contour[contour.n].x1, contour[contour.n].y1 1273 | table.remove(contour) 1274 | contour.n = contour.n - 1 1275 | end 1276 | return contour 1277 | end 1278 | local function contour_to_shape(contour) 1279 | local shape, shape_n, line = {string.format("m %d %d l", contour[1].x1, contour[1].y1)}, 1 1280 | for i=1, contour.n do 1281 | line = contour[i] 1282 | shape_n = shape_n + 1 1283 | shape[shape_n] = string.format("%d %d", line.x2, line.y2) 1284 | end 1285 | return table.concat(shape, " ") 1286 | end 1287 | -- Find shapes for elements 1288 | local element, element_shapes, shape, shape_n, element_contour, element_hole_contour, indices, hole_indices 1289 | local bitmap = {} 1290 | for i=1, elements.n do 1291 | element, element_shapes = elements[i].value, {n = 0} 1292 | -- Create bitmap of data for current element 1293 | for i=1, data_n do 1294 | bitmap[i] = compare_func(data[i], element) and 1 or 0 1295 | end 1296 | -- Find first upper-left element of shapes 1297 | for i=1, data_n do 1298 | if bitmap[i] == 1 then 1299 | -- Detect contour 1300 | element_contour = extract_contour(bitmap, index_to_x(i), index_to_y(i), true) 1301 | indices = contour_indices(element_contour) 1302 | shape, shape_n = {contour_to_shape(merge_contour_lines(element_contour))}, 1 1303 | -- Detect contour holes 1304 | for i=1, indices.n do 1305 | i = indices[i] 1306 | if bitmap[i] == 0 then 1307 | element_hole_contour = extract_contour(bitmap, index_to_x(i), index_to_y(i), false) 1308 | hole_indices = contour_indices(element_hole_contour) 1309 | shape_n = shape_n + 1 1310 | shape[shape_n] = contour_to_shape(merge_contour_lines(element_hole_contour)) 1311 | for i=1, hole_indices.n do 1312 | i = hole_indices[i] 1313 | bitmap[i] = bitmap[i] + 1 1314 | end 1315 | end 1316 | end 1317 | -- Remove contour from bitmap 1318 | for i=1, indices.n do 1319 | i = indices[i] 1320 | bitmap[i] = bitmap[i] - 1 1321 | end 1322 | -- Add shape to element 1323 | element_shapes.n = element_shapes.n + 1 1324 | element_shapes[element_shapes.n] = table.concat(shape, " ") 1325 | end 1326 | end 1327 | -- Set shapes to element 1328 | elements[i].shapes = element_shapes 1329 | end 1330 | -- Return shapes by element 1331 | return elements 1332 | end, 1333 | -- Filters shape points 1334 | filter = function(shape, filter) 1335 | -- Check arguments 1336 | if type(shape) ~= "string" or type(filter) ~= "function" then 1337 | error("shape and filter function expected", 2) 1338 | end 1339 | -- Iterate through space separated tokens 1340 | local token_start, token_end, token, token_num = 1 1341 | local point_start, typ, x, new_point 1342 | repeat 1343 | token_start, token_end, token = shape:find("([^%s]+)", token_start) 1344 | if token_start then 1345 | -- Continue by token type / is number 1346 | token_num = tonumber(token) 1347 | if not token_num then 1348 | -- Set point type 1349 | point_start, typ, x = token_start, token 1350 | else 1351 | -- Set point coordinate 1352 | if not x then 1353 | -- Set x coordinate 1354 | if not point_start then 1355 | point_start = token_start 1356 | end 1357 | x = token_num 1358 | else 1359 | -- Apply filter on completed point 1360 | x, token_num = filter(x, token_num, typ) 1361 | -- Point to replace? 1362 | if type(x) == "number" and type(token_num) == "number" then 1363 | new_point = typ and string.format("%s %s %s", typ, Yutils.math.round(x, FP_PRECISION), Yutils.math.round(token_num, FP_PRECISION)) or 1364 | string.format("%s %s", Yutils.math.round(x, FP_PRECISION), Yutils.math.round(token_num, FP_PRECISION)) 1365 | shape = string.format("%s%s%s", shape:sub(1, point_start-1), new_point, shape:sub(token_end+1)) 1366 | token_end = point_start + #new_point - 1 1367 | end 1368 | -- Reset point / prepare next one 1369 | point_start, typ, x = nil 1370 | end 1371 | end 1372 | -- Increase shape start position to next possible token 1373 | token_start = token_end + 1 1374 | end 1375 | until not token_start 1376 | -- Return (modified) shape 1377 | return shape 1378 | end, 1379 | -- Converts shape curves to lines 1380 | flatten = function(shape) 1381 | -- Check argument 1382 | if type(shape) ~= "string" then 1383 | error("shape expected", 2) 1384 | end 1385 | -- 4th degree curve subdivider 1386 | local function curve4_subdivide(x0, y0, x1, y1, x2, y2, x3, y3, pct) 1387 | -- Calculate points on curve vectors 1388 | 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 1389 | local x012, y012, x123, y123 = (x01+x12)*pct, (y01+y12)*pct, (x12+x23)*pct, (y12+y23)*pct 1390 | local x0123, y0123 = (x012+x123)*pct, (y012+y123)*pct 1391 | -- Return new 2 curves 1392 | return x0, y0, x01, y01, x012, y012, x0123, y0123, 1393 | x0123, y0123, x123, y123, x23, y23, x3, y3 1394 | end 1395 | -- Check flatness of 4th degree curve with angles 1396 | local function curve4_is_flat(x0, y0, x1, y1, x2, y2, x3, y3, tolerance) 1397 | -- Pack curve vectors 1398 | local vecs = {{x1 - x0, y1 - y0}, {x2 - x1, y2 - y1}, {x3 - x2, y3 - y2}} 1399 | -- Remove zero length vectors 1400 | local i, n = 1, #vecs 1401 | while i <= n do 1402 | if vecs[i][1] == 0 and vecs[i][2] == 0 then 1403 | table.remove(vecs, i) 1404 | n = n - 1 1405 | else 1406 | i = i + 1 1407 | end 1408 | end 1409 | -- Check flatness on remaining vectors 1410 | for i=2, n do 1411 | if math.abs(Yutils.math.degree(vecs[i-1][1], vecs[i-1][2], 0, vecs[i][1], vecs[i][2], 0)) > tolerance then 1412 | return false 1413 | end 1414 | end 1415 | return true 1416 | end 1417 | -- Convert 4th degree curve to line points 1418 | local function curve4_to_lines(x0, y0, x1, y1, x2, y2, x3, y3) 1419 | -- Line points buffer 1420 | local pts, pts_n = {x0, y0}, 2 1421 | -- Conversion in recursive processing 1422 | local function convert_recursive(x0, y0, x1, y1, x2, y2, x3, y3) 1423 | if curve4_is_flat(x0, y0, x1, y1, x2, y2, x3, y3, CURVE_TOLERANCE) then 1424 | pts[pts_n+1] = x3 1425 | pts[pts_n+2] = y3 1426 | pts_n = pts_n + 2 1427 | else 1428 | 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) 1429 | convert_recursive(x10, y10, x11, y11, x12, y12, x13, y13) 1430 | convert_recursive(x20, y20, x21, y21, x22, y22, x23, y23) 1431 | end 1432 | end 1433 | convert_recursive(x0, y0, x1, y1, x2, y2, x3, y3) 1434 | -- Return resulting points 1435 | return pts 1436 | end 1437 | -- Search for curves 1438 | local curves_start, curves_end, x0, y0 = 1 1439 | local curve_start, curve_end, x1, y1, x2, y2, x3, y3 1440 | local line_points, line_curve 1441 | repeat 1442 | curves_start, curves_end, x0, y0 = shape:find("([^%s]+)%s+([^%s]+)%s+b%s+", curves_start) 1443 | x0, y0 = tonumber(x0), tonumber(y0) 1444 | -- Curve(s) found! 1445 | if x0 and y0 then 1446 | -- Replace curves type by lines type 1447 | shape = shape:sub(1, curves_start-1) .. shape:sub(curves_start):gsub("b", "l", 1) 1448 | -- Search for single curves 1449 | curve_start = curves_end + 1 1450 | repeat 1451 | 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) 1452 | x1, y1, x2, y2, x3, y3 = tonumber(x1), tonumber(y1), tonumber(x2), tonumber(y2), tonumber(x3), tonumber(y3) 1453 | if x1 and y1 and x2 and y2 and x3 and y3 then 1454 | -- Convert curve to lines 1455 | local line_points = curve4_to_lines(x0, y0, x1, y1, x2, y2, x3, y3) 1456 | for i=1, #line_points do 1457 | line_points[i] = Yutils.math.round(line_points[i], FP_PRECISION) 1458 | end 1459 | line_curve = table.concat(line_points, " ") 1460 | shape = string.format("%s%s%s", shape:sub(1, curve_start-1), line_curve, shape:sub(curve_end+1)) 1461 | curve_end = curve_start + #line_curve - 1 1462 | -- Set next start point to current last point 1463 | x0, y0 = x3, y3 1464 | -- Increase search start position to next possible curve 1465 | curve_start = curve_end + 1 1466 | end 1467 | until not (x1 and y1 and x2 and y2 and x3 and y3) 1468 | -- Increase search start position to next possible curves 1469 | curves_start = curves_end + 1 1470 | end 1471 | until not (x0 and y0) 1472 | -- Return shape without curves 1473 | return shape 1474 | end, 1475 | -- Projects shape on shape 1476 | glue = function(src_shape, dst_shape, transform_callback) 1477 | -- Check arguments 1478 | if type(src_shape) ~= "string" or type(dst_shape) ~= "string" or (transform_callback ~= nil and type(transform_callback) ~= "function") then 1479 | error("2 shapes and optional callback function expected", 2) 1480 | end 1481 | -- Trim destination shape to first figure 1482 | local _, figure_end = dst_shape:find("^%s*m.-m") 1483 | if figure_end then 1484 | dst_shape = dst_shape:sub(1, figure_end - 1) 1485 | end 1486 | -- Collect destination shape/figure lines + lengths 1487 | local dst_lines, dst_lines_n = {}, 0 1488 | local dst_lines_length, dst_line, last_point = 0 1489 | Yutils.shape.filter(Yutils.shape.flatten(dst_shape), function(x, y) 1490 | if last_point then 1491 | 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])} 1492 | if dst_line[5] > 0 then 1493 | dst_lines_n = dst_lines_n + 1 1494 | dst_lines[dst_lines_n] = dst_line 1495 | dst_lines_length = dst_lines_length + dst_line[5] 1496 | end 1497 | end 1498 | last_point = {x, y} 1499 | end) 1500 | -- Any destination line? 1501 | if dst_lines_n > 0 then 1502 | -- Add relative positions to destination lines 1503 | local cur_length = 0 1504 | for _, dst_line in ipairs(dst_lines) do 1505 | dst_line[6] = cur_length / dst_lines_length 1506 | cur_length = cur_length + dst_line[5] 1507 | dst_line[7] = cur_length / dst_lines_length 1508 | end 1509 | -- Get source shape exact bounding box 1510 | local x0, _, x1, y1 = Yutils.shape.bounding(Yutils.shape.flatten(src_shape)) 1511 | -- Source shape has body? 1512 | if x0 and x1 > x0 then 1513 | -- Source shape width 1514 | local w = x1 - x0 1515 | -- Shift source shape on destination shape 1516 | local x_pct, y_off, x_pct_temp, y_off_temp 1517 | local dst_line_pos, ovec_x, ovec_y 1518 | return Yutils.shape.filter(src_shape, function(x, y) 1519 | -- Get relative source point to baseline 1520 | x_pct, y_off = (x - x0) / w, y - y1 1521 | if transform_callback then 1522 | x_pct_temp, y_off_temp = transform_callback(x_pct, y_off) 1523 | if type(x_pct_temp) == "number" and type(y_off_temp) == "number" then 1524 | x_pct, y_off = math.max(0, math.min(x_pct_temp, 1)), y_off_temp 1525 | end 1526 | end 1527 | -- Search for destination point, relative to source point 1528 | for i=1, dst_lines_n do 1529 | dst_line = dst_lines[i] 1530 | if x_pct >= dst_line[6] and x_pct <= dst_line[7] then 1531 | dst_line_pos = (x_pct - dst_line[6]) / (dst_line[7] - dst_line[6]) 1532 | -- Span orthogonal vector to baseline for final source to destination projection 1533 | ovec_x, ovec_y = Yutils.math.ortho(dst_line[3], dst_line[4], 0, 0, 0, -1) 1534 | ovec_x, ovec_y = Yutils.math.stretch(ovec_x, ovec_y, 0, y_off) 1535 | return dst_line[1] + dst_line_pos * dst_line[3] + ovec_x, 1536 | dst_line[2] + dst_line_pos * dst_line[4] + ovec_y 1537 | end 1538 | end 1539 | end) 1540 | end 1541 | end 1542 | end, 1543 | -- Shifts shape coordinates 1544 | move = function(shape, x, y) 1545 | -- Check arguments 1546 | if type(shape) ~= "string" or type(x) ~= "number" or type(y) ~= "number" then 1547 | error("shape, horizontal shift and vertical shift expected", 2) 1548 | end 1549 | -- Shift! 1550 | return Yutils.shape.filter(shape, function(cx, cy) 1551 | return cx + x, cy + y 1552 | end) 1553 | end, 1554 | -- Splits shape lines into shorter segments 1555 | split = function(shape, max_len) 1556 | -- Check arguments 1557 | if type(shape) ~= "string" or type(max_len) ~= "number" or max_len <= 0 then 1558 | error("shape and maximal line length expected", 2) 1559 | end 1560 | -- Remove shape closings (figures become line-completed) 1561 | shape = shape:gsub("%s+c", "") 1562 | -- Line splitter + string encoder 1563 | local function line_split(x0, y0, x1, y1) 1564 | -- Line direction & length 1565 | local rel_x, rel_y = x1 - x0, y1 - y0 1566 | local distance = Yutils.math.distance(rel_x, rel_y) 1567 | -- Line too long -> split! 1568 | if distance > max_len then 1569 | -- Generate line segments 1570 | local lines, lines_n, distance_rest, pct = {}, 0, distance % max_len 1571 | for cur_distance = distance_rest > 0 and distance_rest or max_len, distance, max_len do 1572 | pct = cur_distance / distance 1573 | lines_n = lines_n + 1 1574 | 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)) 1575 | end 1576 | return table.concat(lines, " ") 1577 | -- No line split 1578 | else 1579 | return string.format("%s %s", Yutils.math.round(x1, FP_PRECISION), Yutils.math.round(y1, FP_PRECISION)) 1580 | end 1581 | end 1582 | -- Build new shape with shorter lines 1583 | local new_shape, new_shape_n = {}, 0 1584 | local line_mode, last_point, last_move 1585 | Yutils.shape.filter(shape, function(x, y, typ) 1586 | -- Close last figure of new shape 1587 | if typ == "m" and last_move and not (last_point[1] == last_move[1] and last_point[2] == last_move[2]) then 1588 | if not line_mode then 1589 | new_shape_n = new_shape_n + 1 1590 | new_shape[new_shape_n] = "l" 1591 | end 1592 | new_shape_n = new_shape_n + 1 1593 | new_shape[new_shape_n] = line_split(last_point[1], last_point[2], last_move[1], last_move[2]) 1594 | end 1595 | -- Add current type to new shape 1596 | if typ then 1597 | new_shape_n = new_shape_n + 1 1598 | new_shape[new_shape_n] = typ 1599 | end 1600 | -- En-/disable line mode by current type 1601 | if typ then 1602 | line_mode = typ == "l" 1603 | end 1604 | -- Add current point or splitted line to new shape 1605 | new_shape_n = new_shape_n + 1 1606 | 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)) 1607 | -- Update last point & move 1608 | last_point = {x, y} 1609 | if typ == "m" then 1610 | last_move = {x, y} 1611 | end 1612 | end) 1613 | -- Close last figure of new shape 1614 | if last_move and not (last_point[1] == last_move[1] and last_point[2] == last_move[2]) then 1615 | if not line_mode then 1616 | new_shape_n = new_shape_n + 1 1617 | new_shape[new_shape_n] = "l" 1618 | end 1619 | new_shape_n = new_shape_n + 1 1620 | new_shape[new_shape_n] = line_split(last_point[1], last_point[2], last_move[1], last_move[2]) 1621 | end 1622 | return table.concat(new_shape, " ") 1623 | end, 1624 | -- Converts shape to stroke version 1625 | to_outline = function(shape, width_xy, width_y, mode) 1626 | -- Check arguments 1627 | 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 1628 | error("shape, line width (general or horizontal and vertical) and optional mode expected", 2) 1629 | 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 1630 | error("one width must be >0", 2) 1631 | elseif mode and mode ~= "round" and mode ~= "bevel" and mode ~= "miter" then 1632 | error("valid mode expected", 2) 1633 | end 1634 | -- Line width values 1635 | local width, xscale, yscale 1636 | if width_y and width_xy ~= width_y then 1637 | width = math.max(width_xy, width_y) 1638 | xscale, yscale = width_xy / width, width_y / width 1639 | else 1640 | width, xscale, yscale = width_xy, 1, 1 1641 | end 1642 | -- Collect figures 1643 | local figures, figures_n, figure, figure_n = {}, 0, {}, 0 1644 | local last_move 1645 | Yutils.shape.filter(shape, function(x, y, typ) 1646 | -- Check point type 1647 | if typ and not (typ == "m" or typ == "l") then 1648 | error("shape have to contain only \"moves\" and \"lines\"", 2) 1649 | end 1650 | -- New figure? 1651 | if not last_move or typ == "m" then 1652 | -- Enough points in figure? 1653 | if figure_n > 2 then 1654 | -- Last point equal to first point? (yes: remove him) 1655 | if last_move and figure[figure_n][1] == last_move[1] and figure[figure_n][2] == last_move[2] then 1656 | figure[figure_n] = nil 1657 | end 1658 | -- Save figure 1659 | figures_n = figures_n + 1 1660 | figures[figures_n] = figure 1661 | end 1662 | -- Clear figure for new one 1663 | figure, figure_n = {}, 0 1664 | -- Save last move for figure closing check 1665 | last_move = {x, y} 1666 | end 1667 | -- Add point to current figure (if not copy of last) 1668 | if figure_n == 0 or not(figure[figure_n][1] == x and figure[figure_n][2] == y) then 1669 | figure_n = figure_n + 1 1670 | figure[figure_n] = {x, y} 1671 | end 1672 | end) 1673 | -- Insert last figure (with enough points) 1674 | if figure_n > 2 then 1675 | -- Last point equal to first point? (yes: remove him) 1676 | if last_move and figure[figure_n][1] == last_move[1] and figure[figure_n][2] == last_move[2] then 1677 | figure[figure_n] = nil 1678 | end 1679 | -- Save figure 1680 | figures_n = figures_n + 1 1681 | figures[figures_n] = figure 1682 | end 1683 | -- Create stroke shape out of figures 1684 | local stroke_shape, stroke_shape_n = {}, 0 1685 | for fi, figure in ipairs(figures) do 1686 | -- One pass for inner, one for outer outline 1687 | for i = 1, 2 do 1688 | -- Outline buffer 1689 | local outline, outline_n = {}, 0 1690 | -- Point iteration order = inner or outer outline 1691 | local loop_begin, loop_end, loop_steps 1692 | if i == 1 then 1693 | loop_begin, loop_end, loop_step = #figure, 1, -1 1694 | else 1695 | loop_begin, loop_end, loop_step = 1, #figure, 1 1696 | end 1697 | -- Iterate through figure points 1698 | for pi = loop_begin, loop_end, loop_step do 1699 | -- Collect current, previous and next point 1700 | local point = figure[pi] 1701 | local pre_point, post_point 1702 | if i == 1 then 1703 | if pi == 1 then 1704 | pre_point = figure[pi+1] 1705 | post_point = figure[#figure] 1706 | elseif pi == #figure then 1707 | pre_point = figure[1] 1708 | post_point = figure[pi-1] 1709 | else 1710 | pre_point = figure[pi+1] 1711 | post_point = figure[pi-1] 1712 | end 1713 | else 1714 | if pi == 1 then 1715 | pre_point = figure[#figure] 1716 | post_point = figure[pi+1] 1717 | elseif pi == #figure then 1718 | pre_point = figure[pi-1] 1719 | post_point = figure[1] 1720 | else 1721 | pre_point = figure[pi-1] 1722 | post_point = figure[pi+1] 1723 | end 1724 | end 1725 | -- Calculate orthogonal vectors to both neighbour points 1726 | 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] 1727 | local o_vec1_x, o_vec1_y = Yutils.math.ortho(vec1_x, vec1_y, 0, 0, 0, 1) 1728 | o_vec1_x, o_vec1_y = Yutils.math.stretch(o_vec1_x, o_vec1_y, 0, width) 1729 | local o_vec2_x, o_vec2_y = Yutils.math.ortho(vec2_x, vec2_y, 0, 0, 0, -1) 1730 | o_vec2_x, o_vec2_y = Yutils.math.stretch(o_vec2_x, o_vec2_y, 0, width) 1731 | -- Check for gap or edge join 1732 | local is_x, is_y = Yutils.math.line_intersect(point[1] + o_vec1_x - vec1_x, point[2] + o_vec1_y - vec1_y, 1733 | point[1] + o_vec1_x, point[2] + o_vec1_y, 1734 | point[1] + o_vec2_x - vec2_x, point[2] + o_vec2_y - vec2_y, 1735 | point[1] + o_vec2_x, point[2] + o_vec2_y, 1736 | true) 1737 | if is_y then 1738 | -- Add gap point 1739 | outline_n = outline_n + 1 1740 | outline[outline_n] = string.format("%s%s %s", 1741 | outline_n == 1 and "m " or outline_n == 2 and "l " or "", 1742 | Yutils.math.round(point[1] + (is_x - point[1]) * xscale, FP_PRECISION), Yutils.math.round(point[2] + (is_y - point[2]) * yscale, FP_PRECISION)) 1743 | else 1744 | -- Add first edge point 1745 | outline_n = outline_n + 1 1746 | outline[outline_n] = string.format("%s%s %s", 1747 | outline_n == 1 and "m " or outline_n == 2 and "l " or "", 1748 | Yutils.math.round(point[1] + o_vec1_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + o_vec1_y * yscale, FP_PRECISION)) 1749 | -- Create join by mode 1750 | if mode == "bevel" then 1751 | -- Nothing to add! 1752 | elseif mode == "miter" then 1753 | -- Add mid edge point(s) 1754 | is_x, is_y = Yutils.math.line_intersect(point[1] + o_vec1_x - vec1_x, point[2] + o_vec1_y - vec1_y, 1755 | point[1] + o_vec1_x, point[2] + o_vec1_y, 1756 | point[1] + o_vec2_x - vec2_x, point[2] + o_vec2_y - vec2_y, 1757 | point[1] + o_vec2_x, point[2] + o_vec2_y) 1758 | if is_y then -- Vectors intersect 1759 | local is_vec_x, is_vec_y = is_x - point[1], is_y - point[2] 1760 | local is_vec_len = Yutils.math.distance(is_vec_x, is_vec_y) 1761 | if is_vec_len > MITER_LIMIT then 1762 | local fix_scale = MITER_LIMIT / is_vec_len 1763 | outline_n = outline_n + 1 1764 | outline[outline_n] = string.format("%s%s %s %s %s", 1765 | outline_n == 2 and "l " or "", 1766 | 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), 1767 | 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)) 1768 | else 1769 | outline_n = outline_n + 1 1770 | outline[outline_n] = string.format("%s%s %s", 1771 | outline_n == 2 and "l " or "", 1772 | Yutils.math.round(point[1] + is_vec_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + is_vec_y * yscale, FP_PRECISION)) 1773 | end 1774 | else -- Parallel vectors 1775 | vec1_x, vec1_y = Yutils.math.stretch(vec1_x, vec1_y, 0, MITER_LIMIT) 1776 | vec2_x, vec2_y = Yutils.math.stretch(vec2_x, vec2_y, 0, MITER_LIMIT) 1777 | outline_n = outline_n + 1 1778 | outline[outline_n] = string.format("%s%s %s %s %s", 1779 | outline_n == 2 and "l " or "", 1780 | 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), 1781 | 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)) 1782 | end 1783 | else -- not mode or mode == "round" 1784 | -- Calculate degree & circumference between orthogonal vectors 1785 | local degree = Yutils.math.degree(o_vec1_x, o_vec1_y, 0, o_vec2_x, o_vec2_y, 0) 1786 | local circ = math.abs(math.rad(degree)) * width 1787 | -- Join needed? 1788 | if circ > MAX_CIRCUMFERENCE then 1789 | -- Add curve edge points 1790 | local circ_rest = circ % MAX_CIRCUMFERENCE 1791 | for cur_circ = circ_rest > 0 and circ_rest or MAX_CIRCUMFERENCE, circ - MAX_CIRCUMFERENCE, MAX_CIRCUMFERENCE do 1792 | local curve_vec_x, curve_vec_y = rotate2d(o_vec1_x, o_vec1_y, cur_circ / circ * degree) 1793 | outline_n = outline_n + 1 1794 | outline[outline_n] = string.format("%s%s %s", 1795 | outline_n == 2 and "l " or "", 1796 | Yutils.math.round(point[1] + curve_vec_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + curve_vec_y * yscale, FP_PRECISION)) 1797 | end 1798 | end 1799 | end 1800 | -- Add end edge point 1801 | outline_n = outline_n + 1 1802 | outline[outline_n] = string.format("%s%s %s", 1803 | outline_n == 2 and "l " or "", 1804 | Yutils.math.round(point[1] + o_vec2_x * xscale, FP_PRECISION), Yutils.math.round(point[2] + o_vec2_y * yscale, FP_PRECISION)) 1805 | end 1806 | end 1807 | -- Insert inner or outer outline to stroke shape 1808 | stroke_shape_n = stroke_shape_n + 1 1809 | stroke_shape[stroke_shape_n] = table.concat(outline, " ") 1810 | end 1811 | end 1812 | return table.concat(stroke_shape, " ") 1813 | end, 1814 | -- Converts shape to pixels 1815 | to_pixels = function(shape) 1816 | -- Check argument 1817 | if type(shape) ~= "string" then 1818 | error("shape expected", 2) 1819 | end 1820 | -- Scale values for supersampled rendering 1821 | local upscale = SUPERSAMPLING 1822 | local downscale = 1 / upscale 1823 | -- Upscale shape for later downsampling 1824 | shape = Yutils.shape.filter(shape, function(x, y) 1825 | return x * upscale, y * upscale 1826 | end) 1827 | -- Get shape bounding 1828 | local x1, y1, x2, y2 = Yutils.shape.bounding(shape) 1829 | if not y2 then 1830 | error("not enough shape points", 2) 1831 | end 1832 | -- Bring shape near origin in positive room 1833 | local shift_x, shift_y = -(x1 - x1 % upscale), -(y1 - y1 % upscale) 1834 | shape = Yutils.shape.move(shape, shift_x, shift_y) 1835 | -- Renderer (on binary image with aliasing) 1836 | local function render_shape(width, height, image, shape) 1837 | -- Collect lines (points + vectors) 1838 | local lines, lines_n, last_point, last_move = {}, 0 1839 | Yutils.shape.filter(Yutils.shape.flatten(shape), function(x, y, typ) 1840 | x, y = Yutils.math.round(x), Yutils.math.round(y) -- Use integers to avoid rounding errors 1841 | -- Move 1842 | if typ == "m" then 1843 | -- Close figure with non-horizontal line in image 1844 | 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 1845 | lines_n = lines_n + 1 1846 | lines[lines_n] = {last_point[1], last_point[2], last_move[1] - last_point[1], last_move[2] - last_point[2]} 1847 | end 1848 | last_move = {x, y} 1849 | -- Non-horizontal line in image 1850 | 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 1851 | lines_n = lines_n + 1 1852 | lines[lines_n] = {last_point[1], last_point[2], x - last_point[1], y - last_point[2]} 1853 | end 1854 | -- Remember last point 1855 | last_point = {x, y} 1856 | end) 1857 | -- Close last figure with non-horizontal line in image 1858 | 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 1859 | lines_n = lines_n + 1 1860 | lines[lines_n] = {last_point[1], last_point[2], last_move[1] - last_point[1], last_move[2] - last_point[2]} 1861 | end 1862 | -- Calculates line x horizontal line intersection 1863 | local function line_x_hline(x, y, vx, vy, y2) 1864 | if vy ~= 0 then 1865 | local s = (y2 - y) / vy 1866 | if s >= 0 and s <= 1 then 1867 | return x + s * vx, y2 1868 | end 1869 | end 1870 | end 1871 | -- Scan image rows in shape 1872 | local _, y1, _, y2 = Yutils.shape.bounding(shape) 1873 | for y = math.max(math.floor(y1), 0), math.min(math.ceil(y2), height)-1 do 1874 | -- Collect row intersections with lines 1875 | local row_stops, row_stops_n = {}, 0 1876 | for i=1, lines_n do 1877 | local line = lines[i] 1878 | local cx = line_x_hline(line[1], line[2], line[3], line[4], y + 0.5) 1879 | if cx then 1880 | row_stops_n = row_stops_n + 1 1881 | 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 1882 | end 1883 | end 1884 | -- Enough intersections / something to render? 1885 | if row_stops_n > 1 then 1886 | -- Sort row stops by horizontal position 1887 | table.sort(row_stops, function(a, b) 1888 | return a[1] < b[1] 1889 | end) 1890 | -- Render! 1891 | local status, row_index = 0, 1 + y * width 1892 | for i = 1, row_stops_n-1 do 1893 | status = status + row_stops[i][2] 1894 | if status ~= 0 then 1895 | for x=math.ceil(row_stops[i][1]-0.5), math.floor(row_stops[i+1][1]+0.5)-1 do 1896 | image[row_index + x] = true 1897 | end 1898 | end 1899 | end 1900 | end 1901 | end 1902 | end 1903 | -- Create image 1904 | local img_width, img_height, img_data = math.ceil((x2 + shift_x) * downscale) * upscale, math.ceil((y2 + shift_y) * downscale) * upscale, {} 1905 | for i=1, img_width*img_height do 1906 | img_data[i] = false 1907 | end 1908 | -- Render shape on image 1909 | render_shape(img_width, img_height, img_data, shape) 1910 | -- Extract pixels from image 1911 | local pixels, pixels_n, opacity = {}, 0 1912 | for y=0, img_height-upscale, upscale do 1913 | for x=0, img_width-upscale, upscale do 1914 | opacity = 0 1915 | for yy=0, upscale-1 do 1916 | for xx=0, upscale-1 do 1917 | if img_data[1 + (y+yy) * img_width + (x+xx)] then 1918 | opacity = opacity + 255 1919 | end 1920 | end 1921 | end 1922 | if opacity > 0 then 1923 | pixels_n = pixels_n + 1 1924 | pixels[pixels_n] = { 1925 | alpha = opacity * (downscale * downscale), 1926 | x = (x - shift_x) * downscale, 1927 | y = (y - shift_y) * downscale 1928 | } 1929 | end 1930 | end 1931 | end 1932 | return pixels 1933 | end, 1934 | -- Applies matrix to shape coordinates 1935 | transform = function(shape, matrix) 1936 | -- Check arguments 1937 | if type(shape) ~= "string" or type(matrix) ~= "table" or type(matrix.transform) ~= "function" then 1938 | error("shape and matrix expected", 2) 1939 | end 1940 | local success, x, y, z, w = pcall(matrix.transform, 1, 1, 1) 1941 | if not success or type(x) ~= "number" or type(y) ~= "number" or type(z) ~= "number" or type(w) ~= "number" then 1942 | error("matrix transform method invalid", 2) 1943 | end 1944 | -- Filter shape with matrix 1945 | return Yutils.shape.filter(shape, function(x, y) 1946 | x, y, z, w = matrix.transform(x, y, 0) 1947 | return x / w, y / w 1948 | end) 1949 | end 1950 | }, 1951 | -- Advanced substation alpha sublibrary 1952 | ass = { 1953 | -- Converts between milliseconds and ASS timestamp 1954 | convert_time = function(ass_ms) 1955 | -- Process by argument 1956 | if type(ass_ms) == "number" and ass_ms >= 0 then -- Milliseconds 1957 | return string.format("%d:%02d:%02d.%02d", 1958 | math.floor(ass_ms / 3600000) % 10, 1959 | math.floor(ass_ms % 3600000 / 60000), 1960 | math.floor(ass_ms % 60000 / 1000), 1961 | math.floor(ass_ms % 1000 / 10)) 1962 | elseif type(ass_ms) == "string" and ass_ms:find("^%d:%d%d:%d%d%.%d%d$") then -- ASS timestamp 1963 | 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 1964 | else 1965 | error("milliseconds or ASS timestamp expected", 2) 1966 | end 1967 | end, 1968 | -- Converts between color &/+ alpha numeric and ASS color &/+ alpha 1969 | convert_coloralpha = function(ass_r_a, g, b, a) 1970 | -- Process by argument(s) 1971 | if type(ass_r_a) == "number" and ass_r_a >= 0 and ass_r_a <= 255 then -- Alpha / red numeric 1972 | if type(g) == "number" and g >= 0 and g <= 255 and type(b) == "number" and b >= 0 and b <= 255 then -- Green + blue numeric 1973 | if type(a) == "number" and a >= 0 and a <= 255 then -- Alpha numeric 1974 | return string.format("&H%02X%02X%02X%02X", 255 - a, b, g, ass_r_a) 1975 | else 1976 | return string.format("&H%02X%02X%02X&", b, g, ass_r_a) 1977 | end 1978 | else 1979 | return string.format("&H%02X&", 255 - ass_r_a) 1980 | end 1981 | elseif type(ass_r_a) == "string" then -- ASS value 1982 | if ass_r_a:find("^&H%x%x&$") then -- ASS alpha 1983 | return 255 - tonumber(ass_r_a:sub(3,4), 16) 1984 | elseif ass_r_a:find("^&H%x%x%x%x%x%x&$") then -- ASS color 1985 | 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) 1986 | elseif ass_r_a:find("^&H%x%x%x%x%x%x%x%x$") then -- ASS color+alpha (style) 1987 | 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) 1988 | else 1989 | error("invalid string") 1990 | end 1991 | else 1992 | error("color, alpha or color+alpha as numeric or ASS expected", 2) 1993 | end 1994 | end, 1995 | -- Interpolates between two ASS colors &/+ alphas 1996 | interpolate_coloralpha = function(pct, ...) 1997 | -- Pack arguments 1998 | local args = {...} 1999 | args.n = #args 2000 | -- Check arguments 2001 | if type(pct) ~= "number" or pct < 0 or pct > 1 or args.n < 2 then 2002 | error("progress and at least two ASS values of same type (color, alpha or color+alpha) expected", 2) 2003 | end 2004 | for i=1, args.n do 2005 | if type(args[i]) ~= "string" then 2006 | error("ASS values must be strings", 2) 2007 | end 2008 | end 2009 | -- Pick first ASS value for interpolation 2010 | local i = math.min(1 + math.floor(pct * (args.n-1)), args.n-1) 2011 | -- Extract ASS value parts 2012 | local success1, ass_r_a1, g1, b1, a1 = pcall(Yutils.ass.convert_coloralpha, args[i]) 2013 | local success2, ass_r_a2, g2, b2, a2 = pcall(Yutils.ass.convert_coloralpha, args[i+1]) 2014 | if not success1 or not success2 then 2015 | error("invalid ASS value(s)", 2) 2016 | end 2017 | -- Process by ASS values type 2018 | local min_pct, max_pct = (i-1) / (args.n-1), i / (args.n-1) 2019 | local inner_pct = (pct - min_pct) / (max_pct - min_pct) 2020 | if a1 and a2 then -- Color + alpha 2021 | 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) 2022 | elseif b1 and not a1 and b2 and not a2 then -- Color 2023 | 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) 2024 | elseif not g1 and not g2 then -- Alpha 2025 | return Yutils.ass.convert_coloralpha(ass_r_a1 + (ass_r_a2 - ass_r_a1) * inner_pct) 2026 | else 2027 | error("ASS values must be the same type", 2) 2028 | end 2029 | end, 2030 | -- Creates an ASS parser 2031 | create_parser = function(ass_text) 2032 | -- Check argument 2033 | if ass_text ~= nil and type(ass_text) ~= "string" then 2034 | error("optional string expected", 2) 2035 | end 2036 | -- Current section (for parsing validation) 2037 | local section = "" 2038 | -- ASS contents (just rendering relevant stuff) 2039 | local meta = {wrap_style = 0, scaled_border_and_shadow = true, play_res_x = 0, play_res_y = 0} 2040 | local styles = {} 2041 | local dialogs = {n = 0} 2042 | -- Create parser & getter object 2043 | local obj = { 2044 | parse_line = function(line) 2045 | -- Check argument 2046 | if type(line) ~= "string" then 2047 | error("string expected", 2) 2048 | end 2049 | -- Parse (by) section 2050 | if line:find("^%[.-%]$") then -- Define section 2051 | section = line:sub(2,-2) 2052 | return true 2053 | elseif section == "Script Info" then -- Meta 2054 | if line:find("^WrapStyle: %d$") then 2055 | meta.wrap_style = tonumber(line:sub(12)) 2056 | return true 2057 | elseif line:find("^ScaledBorderAndShadow: %l+$") then 2058 | local value = line:sub(24) 2059 | if value == "yes" or value == "no" then 2060 | meta.scaled_border_and_shadow = value == "yes" 2061 | return true 2062 | end 2063 | elseif line:find("^PlayResX: %d+$") then 2064 | meta.play_res_x = tonumber(line:sub(11)) 2065 | return true 2066 | elseif line:find("^PlayResY: %d+$") then 2067 | meta.play_res_y = tonumber(line:sub(11)) 2068 | return true 2069 | end 2070 | elseif section == "V4+ Styles" then -- Styles 2071 | local name, fontname, fontsize, color1, color2, color3, color4, 2072 | bold, italic, underline, strikeout, scale_x, scale_y, spacing, angle, border_style, 2073 | outline, shadow, alignment, margin_l, margin_r, margin_v, encoding = 2074 | 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+)$") 2075 | if encoding and tonumber(encoding) <= 255 then 2076 | local style = { 2077 | fontname = fontname, 2078 | fontsize = tonumber(fontsize), 2079 | bold = bold == "-1", 2080 | italic = italic == "-1", 2081 | underline = underline == "-1", 2082 | strikeout = strikeout == "-1", 2083 | scale_x = tonumber(scale_x), 2084 | scale_y = tonumber(scale_y), 2085 | spacing = tonumber(spacing), 2086 | angle = tonumber(angle), 2087 | border_style = border_style == "3", 2088 | outline = tonumber(outline), 2089 | shadow = tonumber(shadow), 2090 | alignment = tonumber(alignment), 2091 | margin_l = tonumber(margin_l), 2092 | margin_r = tonumber(margin_r), 2093 | margin_v = tonumber(margin_v), 2094 | encoding = tonumber(encoding) 2095 | } 2096 | local r, g, b, a = Yutils.ass.convert_coloralpha(color1) 2097 | style.color1 = Yutils.ass.convert_coloralpha(r, g, b) 2098 | style.alpha1 = Yutils.ass.convert_coloralpha(a) 2099 | r, g, b, a = Yutils.ass.convert_coloralpha(color2) 2100 | style.color2 = Yutils.ass.convert_coloralpha(r, g, b) 2101 | style.alpha2 = Yutils.ass.convert_coloralpha(a) 2102 | r, g, b, a = Yutils.ass.convert_coloralpha(color3) 2103 | style.color3 = Yutils.ass.convert_coloralpha(r, g, b) 2104 | style.alpha3 = Yutils.ass.convert_coloralpha(a) 2105 | r, g, b, a = Yutils.ass.convert_coloralpha(color4) 2106 | style.color4 = Yutils.ass.convert_coloralpha(r, g, b) 2107 | style.alpha4 = Yutils.ass.convert_coloralpha(a) 2108 | styles[name] = style 2109 | return true 2110 | end 2111 | elseif section == "Events" then -- Dialogs 2112 | local typ, layer, start_time, end_time, style, actor, margin_l, margin_r, margin_v, effect, text = 2113 | line:match("^(.-): (%d+),(%d:%d%d:%d%d%.%d%d),(%d:%d%d:%d%d%.%d%d),(.-),(.-),(%d+%.?%d*),(%d+%.?%d*),(%d+%.?%d*),(.-),(.*)$") 2114 | if text and (typ == "Dialogue" or typ == "Comment") then 2115 | dialogs.n = dialogs.n + 1 2116 | dialogs[dialogs.n] = { 2117 | comment = typ == "Comment", 2118 | layer = tonumber(layer), 2119 | start_time = Yutils.ass.convert_time(start_time), 2120 | end_time = Yutils.ass.convert_time(end_time), 2121 | style = style, 2122 | actor = actor, 2123 | margin_l = tonumber(margin_l), 2124 | margin_r = tonumber(margin_r), 2125 | margin_v = tonumber(margin_v), 2126 | effect = effect, 2127 | text = text 2128 | } 2129 | return true 2130 | end 2131 | end 2132 | -- Nothing parsed 2133 | return false 2134 | end, 2135 | meta = function() 2136 | return Yutils.table.copy(meta) 2137 | end, 2138 | styles = function() 2139 | return Yutils.table.copy(styles) 2140 | end, 2141 | dialogs = function(extended) 2142 | -- Check argument 2143 | if extended ~= nil and type(extended) ~= "boolean" then 2144 | error("optional extension flag expected") 2145 | end 2146 | -- Return extended dialogs 2147 | if extended then 2148 | -- Define text sizes getter 2149 | local function text_sizes(text, style) 2150 | 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) 2151 | local extents, metrics = font.text_extents(text), font.metrics() 2152 | return extents.width, extents.height, metrics.ascent, metrics.descent, metrics.internal_leading, metrics.external_leading 2153 | end 2154 | 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? 2155 | text_sizes = nil 2156 | end 2157 | -- Create dialogs copy & style storage 2158 | local dialogs, dialog_styles, dialog, style_dialogs = Yutils.table.copy(dialogs), {} 2159 | local space_width 2160 | -- Process single dialogs 2161 | for i=1, dialogs.n do 2162 | dialog = dialogs[i] 2163 | -- Append dialog to styles 2164 | style_dialogs = dialog_styles[dialog.style] 2165 | if not style_dialogs then 2166 | style_dialogs = {n = 0} 2167 | dialog_styles[dialog.style] = style_dialogs 2168 | end 2169 | style_dialogs.n = style_dialogs.n + 1 2170 | style_dialogs[style_dialogs.n] = dialog 2171 | -- Add dialog extra informations 2172 | dialog.i = i 2173 | dialog.duration = dialog.end_time - dialog.start_time 2174 | dialog.mid_time = dialog.start_time + dialog.duration / 2 2175 | dialog.styleref = styles[dialog.style] 2176 | dialog.text_stripped = dialog.text:gsub("{.-}", "") 2177 | -- Add dialog text sizes and positions (if possible) 2178 | if text_sizes and dialog.styleref then 2179 | dialog.width, dialog.height, dialog.ascent, dialog.descent, dialog.internal_leading, dialog.external_leading = text_sizes(dialog.text_stripped, dialog.styleref) 2180 | if meta.play_res_x > 0 and meta.play_res_y > 0 then 2181 | -- Horizontal position 2182 | if (dialog.styleref.alignment-1) % 3 == 0 then 2183 | dialog.left = dialog.margin_l ~= 0 and dialog.margin_l or dialog.styleref.margin_l 2184 | dialog.center = dialog.left + dialog.width / 2 2185 | dialog.right = dialog.left + dialog.width 2186 | dialog.x = dialog.left 2187 | elseif (dialog.styleref.alignment-2) % 3 == 0 then 2188 | dialog.left = meta.play_res_x / 2 - dialog.width / 2 2189 | dialog.center = dialog.left + dialog.width / 2 2190 | dialog.right = dialog.left + dialog.width 2191 | dialog.x = dialog.center 2192 | else 2193 | dialog.left = meta.play_res_x - (dialog.margin_r ~= 0 and dialog.margin_r or dialog.styleref.margin_r) - dialog.width 2194 | dialog.center = dialog.left + dialog.width / 2 2195 | dialog.right = dialog.left + dialog.width 2196 | dialog.x = dialog.right 2197 | end 2198 | -- Vertical position 2199 | if dialog.styleref.alignment > 6 then 2200 | dialog.top = dialog.margin_v ~= 0 and dialog.margin_v or dialog.styleref.margin_v 2201 | dialog.middle = dialog.top + dialog.height / 2 2202 | dialog.bottom = dialog.top + dialog.height 2203 | dialog.y = dialog.top 2204 | elseif dialog.styleref.alignment > 3 then 2205 | dialog.top = meta.play_res_y / 2 - dialog.height / 2 2206 | dialog.middle = dialog.top + dialog.height / 2 2207 | dialog.bottom = dialog.top + dialog.height 2208 | dialog.y = dialog.middle 2209 | else 2210 | dialog.top = meta.play_res_y - (dialog.margin_v ~= 0 and dialog.margin_v or dialog.styleref.margin_v) - dialog.height 2211 | dialog.middle = dialog.top + dialog.height / 2 2212 | dialog.bottom = dialog.top + dialog.height 2213 | dialog.y = dialog.bottom 2214 | end 2215 | end 2216 | space_width = text_sizes(" ", dialog.styleref) 2217 | end 2218 | -- Add dialog text chunks 2219 | dialog.text_chunked = {n = 0} 2220 | do 2221 | -- Has tags+text chunks? 2222 | local chunk_start, chunk_end = dialog.text:find("{.-}") 2223 | if not chunk_start then 2224 | dialog.text_chunked = {n = 1, {tags = "", text = dialog.text}} 2225 | else 2226 | -- First chunk without tags 2227 | if chunk_start ~= 1 then 2228 | dialog.text_chunked.n = dialog.text_chunked.n + 1 2229 | dialog.text_chunked[dialog.text_chunked.n] = {tags = "", text = dialog.text:sub(1, chunk_start-1)} 2230 | end 2231 | -- Chunks with tags 2232 | local chunk2_start, chunk2_end 2233 | repeat 2234 | chunk2_start, chunk2_end = dialog.text:find("{.-}", chunk_end+1) 2235 | dialog.text_chunked.n = dialog.text_chunked.n + 1 2236 | 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)} 2237 | chunk_start, chunk_end = chunk2_start, chunk2_end 2238 | until not chunk_start 2239 | end 2240 | end 2241 | -- Add dialog sylables 2242 | dialog.syls = {n = 0} 2243 | do 2244 | local last_time, text_chunk, pretags, kdur, posttags, syl = 0 2245 | -- Get sylables from text chunks 2246 | for i=1, dialog.text_chunked.n do 2247 | text_chunk = dialog.text_chunked[i] 2248 | pretags, kdur, posttags = text_chunk.tags:match("(.-)\\[kK][of]?(%d+)(.*)") 2249 | if posttags then -- All tag groups have to contain karaoke times or everything is invalid (=no sylables there) 2250 | syl = { 2251 | i = dialog.syls.n + 1, 2252 | start_time = last_time, 2253 | mid_time = last_time + kdur * 10 / 2, 2254 | end_time = last_time + kdur * 10, 2255 | duration = kdur * 10, 2256 | tags = pretags .. posttags 2257 | } 2258 | syl.prespace, syl.text, syl.postspace = text_chunk.text:match("(%s*)(%S*)(%s*)") 2259 | syl.prespace, syl.postspace = syl.prespace:len(), syl.postspace:len() 2260 | if text_sizes and dialog.styleref then 2261 | syl.width, syl.height, syl.ascent, syl.descent, syl.internal_leading, syl.external_leading = text_sizes(syl.text, dialog.styleref) 2262 | end 2263 | last_time = syl.end_time 2264 | dialog.syls.n = dialog.syls.n + 1 2265 | dialog.syls[dialog.syls.n] = syl 2266 | else 2267 | dialog.syls = {n = 0} 2268 | break 2269 | end 2270 | end 2271 | -- Calculate sylable positions with all sylables data already available 2272 | if dialog.syls.n > 0 and dialog.syls[1].width and meta.play_res_x > 0 and meta.play_res_y > 0 then 2273 | if dialog.styleref.alignment > 6 or dialog.styleref.alignment < 4 then 2274 | local cur_x = dialog.left 2275 | for i=1, dialog.syls.n do 2276 | syl = dialog.syls[i] 2277 | -- Horizontal position 2278 | cur_x = cur_x + syl.prespace * space_width 2279 | syl.left = cur_x 2280 | syl.center = syl.left + syl.width / 2 2281 | syl.right = syl.left + syl.width 2282 | syl.x = (dialog.styleref.alignment-1) % 3 == 0 and syl.left or 2283 | (dialog.styleref.alignment-2) % 3 == 0 and syl.center or 2284 | syl.right 2285 | cur_x = cur_x + syl.width + syl.postspace * space_width 2286 | -- Vertical position 2287 | syl.top = dialog.top 2288 | syl.middle = dialog.middle 2289 | syl.bottom = dialog.bottom 2290 | syl.y = dialog.y 2291 | end 2292 | else 2293 | local max_width, sum_height = 0, 0 2294 | for i=1, dialog.syls.n do 2295 | syl = dialog.syls[i] 2296 | max_width = math.max(max_width, syl.width) 2297 | sum_height = sum_height + syl.height 2298 | end 2299 | local cur_y, x_fix = meta.play_res_y / 2 - sum_height / 2 2300 | for i=1, dialog.syls.n do 2301 | syl = dialog.syls[i] 2302 | -- Horizontal position 2303 | x_fix = (max_width - syl.width) / 2 2304 | if dialog.styleref.alignment == 4 then 2305 | syl.left = dialog.left + x_fix 2306 | syl.center = syl.left + syl.width / 2 2307 | syl.right = syl.left + syl.width 2308 | syl.x = syl.left 2309 | elseif dialog.styleref.alignment == 5 then 2310 | syl.left = meta.play_res_x / 2 - syl.width / 2 2311 | syl.center = syl.left + syl.width / 2 2312 | syl.right = syl.left + syl.width 2313 | syl.x = syl.center 2314 | else -- dialog.styleref.alignment == 6 2315 | syl.left = dialog.right - syl.width - x_fix 2316 | syl.center = syl.left + syl.width / 2 2317 | syl.right = syl.left + syl.width 2318 | syl.x = syl.right 2319 | end 2320 | -- Vertical position 2321 | syl.top = cur_y 2322 | syl.middle = syl.top + syl.height / 2 2323 | syl.bottom = syl.top + syl.height 2324 | syl.y = syl.middle 2325 | cur_y = cur_y + syl.height 2326 | end 2327 | end 2328 | end 2329 | end 2330 | -- Add dialog words 2331 | dialog.words = {n = 0} 2332 | do 2333 | local word 2334 | for prespace, word_text, postspace in dialog.text_stripped:gmatch("(%s*)(%S+)(%s*)") do 2335 | word = { 2336 | i = dialog.words.n + 1, 2337 | start_time = dialog.start_time, 2338 | mid_time = dialog.mid_time, 2339 | end_time = dialog.end_time, 2340 | duration = dialog.duration, 2341 | text = word_text, 2342 | prespace = prespace:len(), 2343 | postspace = postspace:len() 2344 | } 2345 | if text_sizes and dialog.styleref then 2346 | word.width, word.height, word.ascent, word.descent, word.internal_leading, word.external_leading = text_sizes(word.text, dialog.styleref) 2347 | end 2348 | -- Add current word to dialog words 2349 | dialog.words.n = dialog.words.n + 1 2350 | dialog.words[dialog.words.n] = word 2351 | end 2352 | -- Calculate word positions with all words data already available 2353 | if dialog.words.n > 0 and dialog.words[1].width and meta.play_res_x > 0 and meta.play_res_y > 0 then 2354 | if dialog.styleref.alignment > 6 or dialog.styleref.alignment < 4 then 2355 | local cur_x = dialog.left 2356 | for i=1, dialog.words.n do 2357 | word = dialog.words[i] 2358 | -- Horizontal position 2359 | cur_x = cur_x + word.prespace * space_width 2360 | word.left = cur_x 2361 | word.center = word.left + word.width / 2 2362 | word.right = word.left + word.width 2363 | word.x = (dialog.styleref.alignment-1) % 3 == 0 and word.left or 2364 | (dialog.styleref.alignment-2) % 3 == 0 and word.center or 2365 | word.right 2366 | cur_x = cur_x + word.width + word.postspace * space_width 2367 | -- Vertical position 2368 | word.top = dialog.top 2369 | word.middle = dialog.middle 2370 | word.bottom = dialog.bottom 2371 | word.y = dialog.y 2372 | end 2373 | else 2374 | local max_width, sum_height = 0, 0 2375 | for i=1, dialog.words.n do 2376 | word = dialog.words[i] 2377 | max_width = math.max(max_width, word.width) 2378 | sum_height = sum_height + word.height 2379 | end 2380 | local cur_y, x_fix = meta.play_res_y / 2 - sum_height / 2 2381 | for i=1, dialog.words.n do 2382 | word = dialog.words[i] 2383 | -- Horizontal position 2384 | x_fix = (max_width - word.width) / 2 2385 | if dialog.styleref.alignment == 4 then 2386 | word.left = dialog.left + x_fix 2387 | word.center = word.left + word.width / 2 2388 | word.right = word.left + word.width 2389 | word.x = word.left 2390 | elseif dialog.styleref.alignment == 5 then 2391 | word.left = meta.play_res_x / 2 - word.width / 2 2392 | word.center = word.left + word.width / 2 2393 | word.right = word.left + word.width 2394 | word.x = word.center 2395 | else -- dialog.styleref.alignment == 6 2396 | word.left = dialog.right - word.width - x_fix 2397 | word.center = word.left + word.width / 2 2398 | word.right = word.left + word.width 2399 | word.x = word.right 2400 | end 2401 | -- Vertical position 2402 | word.top = cur_y 2403 | word.middle = word.top + word.height / 2 2404 | word.bottom = word.top + word.height 2405 | word.y = word.middle 2406 | cur_y = cur_y + word.height 2407 | end 2408 | end 2409 | end 2410 | end 2411 | -- Add dialog characters 2412 | dialog.chars = {n = 0} 2413 | do 2414 | local char, char_index, syl, word 2415 | for _, char_text in Yutils.utf8.chars(dialog.text_stripped) do 2416 | char = { 2417 | i = dialog.chars.n + 1, 2418 | start_time = dialog.start_time, 2419 | mid_time = dialog.mid_time, 2420 | end_time = dialog.end_time, 2421 | duration = dialog.duration, 2422 | text = char_text 2423 | } 2424 | char_index = 0 2425 | for i=1, dialog.syls.n do 2426 | syl = dialog.syls[i] 2427 | for _ in Yutils.utf8.chars(string.format("%s%s%s", string.rep(" ", syl.prespace), syl.text, string.rep(" ", syl.postspace))) do 2428 | char_index = char_index + 1 2429 | if char_index == char.i then 2430 | char.syl_i = syl.i 2431 | char.start_time = syl.start_time 2432 | char.mid_time = syl.mid_time 2433 | char.end_time = syl.end_time 2434 | char.duration = syl.duration 2435 | goto syl_reference_found 2436 | end 2437 | end 2438 | end 2439 | ::syl_reference_found:: 2440 | char_index = 0 2441 | for i=1, dialog.words.n do 2442 | word = dialog.words[i] 2443 | for _ in Yutils.utf8.chars(string.format("%s%s%s", string.rep(" ", word.prespace), word.text, string.rep(" ", word.postspace))) do 2444 | char_index = char_index + 1 2445 | if char_index == char.i then 2446 | char.word_i = word.i 2447 | goto word_reference_found 2448 | end 2449 | end 2450 | end 2451 | ::word_reference_found:: 2452 | if text_sizes and dialog.styleref then 2453 | char.width, char.height, char.ascent, char.descent, char.internal_leading, char.external_leading = text_sizes(char.text, dialog.styleref) 2454 | end 2455 | dialog.chars.n = dialog.chars.n + 1 2456 | dialog.chars[dialog.chars.n] = char 2457 | end 2458 | -- Calculate character positions with all characters data already available 2459 | if dialog.chars.n > 0 and dialog.chars[1].width and meta.play_res_x > 0 and meta.play_res_y > 0 then 2460 | if dialog.styleref.alignment > 6 or dialog.styleref.alignment < 4 then 2461 | local cur_x = dialog.left 2462 | for i=1, dialog.chars.n do 2463 | char = dialog.chars[i] 2464 | -- Horizontal position 2465 | char.left = cur_x 2466 | char.center = char.left + char.width / 2 2467 | char.right = char.left + char.width 2468 | char.x = (dialog.styleref.alignment-1) % 3 == 0 and char.left or 2469 | (dialog.styleref.alignment-2) % 3 == 0 and char.center or 2470 | char.right 2471 | cur_x = cur_x + char.width 2472 | -- Vertical position 2473 | char.top = dialog.top 2474 | char.middle = dialog.middle 2475 | char.bottom = dialog.bottom 2476 | char.y = dialog.y 2477 | end 2478 | else 2479 | local max_width, sum_height = 0, 0 2480 | for i=1, dialog.chars.n do 2481 | char = dialog.chars[i] 2482 | max_width = math.max(max_width, char.width) 2483 | sum_height = sum_height + char.height 2484 | end 2485 | local cur_y, x_fix = meta.play_res_y / 2 - sum_height / 2 2486 | for i=1, dialog.chars.n do 2487 | char = dialog.chars[i] 2488 | -- Horizontal position 2489 | x_fix = (max_width - char.width) / 2 2490 | if dialog.styleref.alignment == 4 then 2491 | char.left = dialog.left + x_fix 2492 | char.center = char.left + char.width / 2 2493 | char.right = char.left + char.width 2494 | char.x = char.left 2495 | elseif dialog.styleref.alignment == 5 then 2496 | char.left = meta.play_res_x / 2 - char.width / 2 2497 | char.center = char.left + char.width / 2 2498 | char.right = char.left + char.width 2499 | char.x = char.center 2500 | else -- dialog.styleref.alignment == 6 2501 | char.left = dialog.right - char.width - x_fix 2502 | char.center = char.left + char.width / 2 2503 | char.right = char.left + char.width 2504 | char.x = char.right 2505 | end 2506 | -- Vertical position 2507 | char.top = cur_y 2508 | char.middle = char.top + char.height / 2 2509 | char.bottom = char.top + char.height 2510 | char.y = char.middle 2511 | cur_y = cur_y + char.height 2512 | end 2513 | end 2514 | end 2515 | end 2516 | end 2517 | -- Add durations between dialogs 2518 | for _, dialogs in pairs(dialog_styles) do 2519 | table.sort(dialogs, function(dialog1, dialog2) return dialog1.start_time <= dialog2.start_time end) 2520 | for i=1, dialogs.n do 2521 | dialog = dialogs[i] 2522 | dialog.leadin = i == 1 and 1000.1 or dialog.start_time - dialogs[i-1].end_time 2523 | dialog.leadout = i == dialogs.n and 1000.1 or dialogs[i+1].start_time - dialog.end_time 2524 | end 2525 | end 2526 | -- Return modified copy 2527 | return dialogs 2528 | -- Return raw dialogs 2529 | else 2530 | return Yutils.table.copy(dialogs) 2531 | end 2532 | end 2533 | } 2534 | -- Parse ASS text 2535 | if ass_text then 2536 | for line in Yutils.algorithm.lines(ass_text) do 2537 | obj.parse_line(line) -- no errors possible 2538 | end 2539 | end 2540 | -- Return object 2541 | return obj 2542 | end 2543 | }, 2544 | -- Decoder sublibrary 2545 | decode = { 2546 | -- Creates BMP file reader 2547 | create_bmp_reader = function(filename) 2548 | -- Check argument 2549 | if type(filename) ~= "string" then 2550 | error("bitmap filename expected", 2) 2551 | end 2552 | -- Image decoders 2553 | local function bmp_decode(filename) 2554 | -- Open file handle 2555 | local file = io.open(filename, "rb") 2556 | if file then 2557 | -- Read file header 2558 | local header = file:read(14) 2559 | if not header or #header ~= 14 then 2560 | return "couldn't read file header" 2561 | end 2562 | -- Check BMP signature 2563 | if header:sub(1,2) == "BM" then 2564 | -- Read relevant file header fields 2565 | local file_size, data_offset = bton(header:sub(3,6)), bton(header:sub(11,14)) 2566 | -- Read DIB header 2567 | header = file:read(24) 2568 | if not header or #header ~= 24 then 2569 | return "couldn't read DIB header" 2570 | end 2571 | -- Read relevant DIB header fields 2572 | 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)) 2573 | -- Check read header data 2574 | if width >= 2^31 then 2575 | return "pixels in right-to-left order are not supported" 2576 | elseif planes ~= 1 then 2577 | return "planes must be 1" 2578 | elseif bit_depth ~= 24 and bit_depth ~= 32 then 2579 | return "bit depth must be 24 or 32" 2580 | elseif compression ~= 0 then 2581 | return "must be uncompressed RGB" 2582 | elseif data_size == 0 then 2583 | return "data size must not be zero" 2584 | end 2585 | -- Fix read header data 2586 | if height >= 2^31 then 2587 | height = height - 2^32 2588 | end 2589 | -- Read image data 2590 | file:seek("set", data_offset) 2591 | local data = file:read(data_size) 2592 | if not data or #data ~= data_size then 2593 | return "not enough data" 2594 | end 2595 | -- Calculate row size (round up to multiple of 4) 2596 | local row_size = math.floor((bit_depth * width + 31) / 32) * 4 2597 | -- All data read from file -> close handle (don't wait for GC) 2598 | file:close() 2599 | -- Return relevant bitmap informations 2600 | return file_size, width, height, bit_depth, data_size, data, row_size 2601 | end 2602 | end 2603 | end 2604 | local function png_decode(filename) 2605 | -- PNG decode library available? 2606 | if libpng then 2607 | -- Open file handle 2608 | local file = io.open(filename, "rb") 2609 | if file then 2610 | -- Load file content & close no further needed file handle 2611 | local file_content = file:read("*a") 2612 | file:close() 2613 | -- Get file size 2614 | local file_size = #file_content 2615 | -- Check PNG signature 2616 | 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 2617 | -- Create PNG data structures & set error handlers 2618 | local ppng, pinfo, err = ffi.new("png_structp[1]"), ffi.new("png_infop[1]") 2619 | local function err_func(png, message) 2620 | libpng.png_destroy_read_struct(ppng, pinfo, nil) 2621 | err = ffi.string(message) 2622 | end 2623 | ppng[0] = libpng.png_create_read_struct(ffi.cast("char*", "1.5.14"), nil, err_func, err_func) 2624 | if not ppng[0] then 2625 | return "couldn't create png read structure" 2626 | end 2627 | pinfo[0] = libpng.png_create_info_struct(ppng[0]) 2628 | if not pinfo[0] then 2629 | libpng.png_destroy_read_struct(ppng, nil, nil) 2630 | return "couldn't create png info structure" 2631 | end 2632 | -- Decode file content to png structures 2633 | local file_pos, file_content_bytes = 0, ffi.cast("png_bytep", file_content) 2634 | libpng.png_set_read_fn(ppng[0], nil, function(png, output_bytes, required_bytes) 2635 | if file_pos + required_bytes <= file_size then 2636 | ffi.C.memcpy(output_bytes, file_content_bytes+file_pos, required_bytes) 2637 | file_pos = file_pos + required_bytes 2638 | end 2639 | end) 2640 | 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) 2641 | if err then 2642 | return err 2643 | end 2644 | libpng.png_set_interlace_handling(ppng[0]) 2645 | libpng.png_read_update_info(ppng[0], pinfo[0]) 2646 | if err then 2647 | return err 2648 | end 2649 | -- Get header data 2650 | 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]) 2651 | local data_size, bit_depth = height * row_size 2652 | if color_type == ffi.C.PNG_COLOR_TYPE_RGB then 2653 | bit_depth = 24 2654 | elseif color_type == ffi.C.PNG_COLOR_TYPE_RGBA then 2655 | bit_depth = 32 2656 | else 2657 | libpng.png_destroy_read_struct(ppng, pinfo, nil) 2658 | return "png data conversion to BGR(A) colorspace failed" 2659 | end 2660 | -- Get image data 2661 | local rows = libpng.png_get_rows(ppng[0], pinfo[0]) 2662 | local data, data_n = {}, 0 2663 | for i=0, height-1 do 2664 | data_n = data_n + 1 2665 | data[data_n] = ffi.string(rows[i], row_size) 2666 | end 2667 | data = table.concat(data) 2668 | -- Clean up 2669 | libpng.png_destroy_read_struct(ppng, pinfo, nil) 2670 | -- Return relevant bitmap informations 2671 | return file_size, width, height, bit_depth, data_size, data, row_size 2672 | end 2673 | end 2674 | end 2675 | end 2676 | -- Try to decode file 2677 | local bottom_up 2678 | local file_size, width, height, bit_depth, data_size, data, row_size = bmp_decode(filename) 2679 | if not file_size then 2680 | file_size, width, height, bit_depth, data_size, data, row_size = png_decode(filename) 2681 | if not file_size then 2682 | error("couldn't decode file", 2) 2683 | elseif type(file_size) == "string" then 2684 | error(file_size, 2) 2685 | else 2686 | bottom_up = false 2687 | end 2688 | elseif type(file_size) == "string" then 2689 | error(file_size, 2) 2690 | else 2691 | bottom_up = height >= 0 2692 | height = math.abs(height) 2693 | end 2694 | -- Return bitmap object 2695 | local obj 2696 | obj = { 2697 | file_size = function() 2698 | return file_size 2699 | end, 2700 | width = function() 2701 | return width 2702 | end, 2703 | height = function() 2704 | return height 2705 | end, 2706 | bit_depth = function() 2707 | return bit_depth 2708 | end, 2709 | data_size = function() 2710 | return data_size 2711 | end, 2712 | row_size = function() 2713 | return row_size 2714 | end, 2715 | bottom_up = function() 2716 | return bottom_up 2717 | end, 2718 | data_raw = function() 2719 | return data 2720 | end, 2721 | data_packed = function() 2722 | local data_packed, data_packed_n = {}, 0 2723 | local first_row, last_row, row_step 2724 | if bottom_up then 2725 | first_row, last_row, row_step = height-1, 0, -1 2726 | else 2727 | first_row, last_row, row_step = 0, height-1, 1 2728 | end 2729 | if bit_depth == 24 then 2730 | local last_row_item, r, g, b = (width-1)*3 2731 | for y=first_row, last_row, row_step do 2732 | y = 1 + y * row_size 2733 | for x=0, last_row_item, 3 do 2734 | b, g, r = data:byte(y+x, y+x+2) 2735 | data_packed_n = data_packed_n + 1 2736 | data_packed[data_packed_n] = { 2737 | r = r, 2738 | g = g, 2739 | b = b, 2740 | a = 255 2741 | } 2742 | end 2743 | end 2744 | else -- bit_depth == 32 2745 | local last_row_item, r, g, b, a = (width-1)*4 2746 | for y=first_row, last_row, row_step do 2747 | y = 1 + y * row_size 2748 | for x=0, last_row_item, 4 do 2749 | b, g, r, a = data:byte(y+x, y+x+3) 2750 | data_packed_n = data_packed_n + 1 2751 | data_packed[data_packed_n] = { 2752 | r = r, 2753 | g = g, 2754 | b = b, 2755 | a = a 2756 | } 2757 | end 2758 | end 2759 | end 2760 | return data_packed 2761 | end, 2762 | data_text = function() 2763 | local data_pack, text, text_n = obj.data_packed(), {"{\\bord0\\shad0\\an7\\p1}"}, 1 2764 | local x, y, off_x, chunk_size, color1, color2 = 0, 0, 0 2765 | local i, n = 1, #data_pack 2766 | while i <= n do 2767 | if x == width then 2768 | x = 0 2769 | y = y + 1 2770 | off_x = off_x - width 2771 | end 2772 | chunk_size, color1, text_n = 1, data_pack[i], text_n + 1 2773 | if color1.a == 0 then 2774 | for xx=x+1, width-1 do 2775 | color2 = data_pack[i+(xx-x)] 2776 | if not (color2 and color2.a == 0) then 2777 | break 2778 | end 2779 | chunk_size = chunk_size + 1 2780 | end 2781 | text[text_n] = string.format("{}m %d %d l %d %d", off_x, y, off_x+chunk_size, y+1) 2782 | else 2783 | for xx=x+1, width-1 do 2784 | color2 = data_pack[i+(xx-x)] 2785 | if not (color2 and color1.r == color2.r and color1.g == color2.g and color1.b == color2.b and color1.a == color2.a) then 2786 | break 2787 | end 2788 | chunk_size = chunk_size + 1 2789 | end 2790 | text[text_n] = string.format("{\\c&H%02X%02X%02X&\\1a&H%02X&}m %d %d l %d %d %d %d %d %d", 2791 | 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) 2792 | end 2793 | i, x = i + chunk_size, x + chunk_size 2794 | end 2795 | return table.concat(text) 2796 | end 2797 | } 2798 | return obj 2799 | end, 2800 | -- Create WAV file reader 2801 | create_wav_reader = function(filename) 2802 | -- Check argument 2803 | if type(filename) ~= "string" then 2804 | error("audio filename expected", 2) 2805 | end 2806 | -- Open file handle 2807 | local file = io.open(filename, "rb") 2808 | if not file then 2809 | error("couldn't open file", 2) 2810 | end 2811 | -- Read file header 2812 | local header = file:read(12) 2813 | if not header or #header ~= 12 then 2814 | error("couldn't read file header", 2) 2815 | -- Check WAVE signature 2816 | elseif header:sub(1,4) ~= "RIFF" or header:sub(9,12) ~= "WAVE" then 2817 | error("not a wave file", 2) 2818 | end 2819 | -- Data to save (+ read relevant file header field) 2820 | local file_size, channels_number, sample_rate, byte_rate, block_align, bits_per_sample = bton(header:sub(5,8)) + 8 -- remaining + already read bytes 2821 | local data_begin, data_end 2822 | -- Read file chunks 2823 | local chunk_type, chunk_size 2824 | while true do 2825 | -- Read single chunk 2826 | chunk_type, chunk_size = file:read(4), file:read(4) 2827 | if not chunk_size or #chunk_size ~= 4 then 2828 | break 2829 | end 2830 | chunk_size = bton(chunk_size) 2831 | -- Identify chunk type 2832 | if chunk_type == "fmt " then 2833 | -- Read format informations 2834 | header = file:read(16) 2835 | if chunk_size < 16 or not header or #header ~= 16 then 2836 | error("format chunk corrupted", 2) 2837 | elseif bton(header:sub(1,2)) ~= 1 then 2838 | error("data must be in PCM format", 2) 2839 | end 2840 | 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)) 2841 | if bits_per_sample ~= 8 and bits_per_sample ~= 16 and bits_per_sample ~= 24 and bits_per_sample ~= 32 then 2842 | error("bits per sample must be 8, 16, 24 or 32", 2) 2843 | elseif channels_number == 0 or sample_rate == 0 or byte_rate == 0 or block_align == 0 then 2844 | error("invalid format data", 2) 2845 | end 2846 | file:seek("cur", chunk_size-16) 2847 | elseif chunk_type == "data" then 2848 | -- Save samples reference 2849 | data_begin = file:seek() 2850 | data_end = data_begin + chunk_size 2851 | file:seek("cur", chunk_size) 2852 | else 2853 | -- Skip chunk 2854 | file:seek("cur", chunk_size) 2855 | end 2856 | end 2857 | -- Check all needed data are read 2858 | if not bits_per_sample or not data_end then 2859 | error("format or data are missing", 2) 2860 | end 2861 | -- Calculate extra data 2862 | local samples_per_channel = (data_end - data_begin) / block_align 2863 | -- Set file pointer ready for data reading 2864 | file:seek("set", data_begin) 2865 | -- Return wave object 2866 | local obj 2867 | obj = { 2868 | file_size = function() 2869 | return file_size 2870 | end, 2871 | channels_number = function() 2872 | return channels_number 2873 | end, 2874 | sample_rate = function() 2875 | return sample_rate 2876 | end, 2877 | byte_rate = function() 2878 | return byte_rate 2879 | end, 2880 | block_align = function() 2881 | return block_align 2882 | end, 2883 | bits_per_sample = function() 2884 | return bits_per_sample 2885 | end, 2886 | samples_per_channel = function() 2887 | return samples_per_channel 2888 | end, 2889 | min_max_amplitude = function() 2890 | local half_level = 2^bits_per_sample / 2 2891 | return -half_level, half_level-1 2892 | end, 2893 | sample_from_ms = function(ms) 2894 | if type(ms) ~= "number" or ms < 0 then 2895 | error("positive number expected", 2) 2896 | end 2897 | return ms * 0.001 * sample_rate 2898 | end, 2899 | ms_from_sample = function(sample) 2900 | if type(sample) ~= "number" or sample < 0 then 2901 | error("positive number expected", 2) 2902 | end 2903 | return sample / sample_rate * 1000 2904 | end, 2905 | position = function(pos) 2906 | if pos ~= nil and (type(pos) ~= "number" or pos < 0) then 2907 | error("optional positive number expected", 2) 2908 | elseif pos then 2909 | file:seek("set", data_begin + pos * block_align) 2910 | end 2911 | return (file:seek() - data_begin) / block_align 2912 | end, 2913 | samples_interlaced = function(n) 2914 | if type(n) ~= "number" or math.floor(n) < 1 then 2915 | error("positive number greater-equal one expected", 2) 2916 | end 2917 | local output, bytes = {n = 0}, file:read(math.floor(n) * block_align) 2918 | if bytes then 2919 | local bytes_per_sample, sample = bits_per_sample / 8 2920 | local max_amplitude, amplitude_fix = ({127, 32767, 8388607, 2147483647})[bytes_per_sample], ({256, 65536, 16777216, 4294967296})[bytes_per_sample] 2921 | for i=1, #bytes, bytes_per_sample do 2922 | sample = bton(bytes:sub(i,i+bytes_per_sample-1)) 2923 | output.n = output.n + 1 2924 | output[output.n] = sample > max_amplitude and sample - amplitude_fix or sample 2925 | end 2926 | end 2927 | return output 2928 | end, 2929 | samples = function(n) 2930 | local success, samples = pcall(obj.samples_interlaced, n) 2931 | if not success then 2932 | error(samples, 2) 2933 | end 2934 | local output, channel_samples = {n = channels_number} 2935 | for c=1, output.n do 2936 | channel_samples = {n = math.floor(samples.n / channels_number)} 2937 | for s=1, channel_samples.n do 2938 | channel_samples[s] = samples[c + (s-1) * channels_number] 2939 | end 2940 | output[c] = channel_samples 2941 | end 2942 | return output 2943 | end 2944 | } 2945 | return obj 2946 | end, 2947 | create_frequency_analyzer = function(samples, sample_rate) 2948 | -- Check arguments 2949 | if type(samples) ~= "table" or type(sample_rate) ~= "number" or sample_rate < 2 or sample_rate % 2 ~= 0 then 2950 | error("samples table and sample rate expected", 2) 2951 | end 2952 | local samples_n = #samples 2953 | if samples_n < 2 then 2954 | error("not enough samples", 2) 2955 | end 2956 | local sample 2957 | for i=1, samples_n do 2958 | sample = samples[i] 2959 | if type(sample) ~= "number" then 2960 | error("samples have to be numbers", 2) 2961 | elseif sample < -1 or sample > 1 then 2962 | error("samples have to be in range -1 <> 1", 2) 2963 | end 2964 | end 2965 | -- Fix samples number to power of 2 for further processing 2966 | samples_n = 2^math.floor(math.log(samples_n, 2)) 2967 | -- Complex numbers 2968 | local complex_t 2969 | do 2970 | local complex = {} 2971 | complex_t = function(r, i) 2972 | return setmetatable({r = r, i = i}, complex) 2973 | end 2974 | local function tocomplex(a, b) 2975 | if getmetatable(a) ~= complex then return {r = a, i = 0}, b 2976 | elseif getmetatable(b) ~= complex then return a, {r = b, i = 0} 2977 | else return a, b end 2978 | end 2979 | complex.__add = function(a, b) 2980 | local c1, c2 = tocomplex(a, b) 2981 | return complex_t(c1.r + c2.r, c1.i + c2.i) 2982 | end 2983 | complex.__sub = function(a, b) 2984 | local c1, c2 = tocomplex(a, b) 2985 | return complex_t(c1.r - c2.r, c1.i - c2.i) 2986 | end 2987 | complex.__mul = function(a, b) 2988 | local c1, c2 = tocomplex(a, b) 2989 | return complex_t(c1.r * c2.r - c1.i * c2.i, c1.r * c2.i + c1.i * c2.r) 2990 | end 2991 | end 2992 | local function polar(theta) 2993 | return complex_t(math.cos(theta), math.sin(theta)) 2994 | end 2995 | local function magnitude(c) 2996 | return math.sqrt(c.r^2 + c.i^2) 2997 | end 2998 | -- Fast Fourier Transformation 2999 | local function fft(x) 3000 | -- Check recursion break 3001 | local N = x.n 3002 | if N > 1 then 3003 | -- Divide 3004 | local even, odd = {n = 0}, {n = 0} 3005 | for i=1, N, 2 do 3006 | even.n = even.n + 1 3007 | even[even.n] = x[i] 3008 | end 3009 | for i=2, N, 2 do 3010 | odd.n = odd.n + 1 3011 | odd[odd.n] = x[i] 3012 | end 3013 | -- Conquer 3014 | fft(even) 3015 | fft(odd) 3016 | --Combine 3017 | local t 3018 | for k = 1, N/2 do 3019 | t = polar(-2 * math.pi * (k-1) / N) * odd[k] 3020 | x[k] = even[k] + t 3021 | x[k+N/2] = even[k] - t 3022 | end 3023 | end 3024 | end 3025 | -- Samples to complex numbers 3026 | local data = {n = samples_n} 3027 | for i = 1, data.n do 3028 | data[i] = complex_t(samples[i], 0) 3029 | end 3030 | -- Process FFT 3031 | fft(data) 3032 | -- Complex numbers to frequencies domain data 3033 | for i = 1, data.n do 3034 | data[i] = magnitude(data[i]) 3035 | end 3036 | -- Extract frequencies weights 3037 | local frequencies, frequency_sum, sample_rate_half = {n = data.n / 2}, 0, sample_rate / 2 3038 | for i=1, frequencies.n do 3039 | frequency_sum = frequency_sum + data[i] 3040 | end 3041 | if frequency_sum == 0 then 3042 | frequencies[1] = {freq = 0, weight = 1} 3043 | for i=2, frequencies.n do 3044 | frequencies[i] = {freq = (i-1) / (frequencies.n-1) * sample_rate_half, weight = 0} 3045 | end 3046 | else 3047 | for i=1, frequencies.n do 3048 | frequencies[i] = {freq = (i-1) / (frequencies.n-1) * sample_rate_half, weight = data[i] / frequency_sum} 3049 | end 3050 | end 3051 | -- Return frequencies object 3052 | return { 3053 | frequencies = function() 3054 | return Yutils.table.copy(frequencies) 3055 | end, 3056 | frequency_weight = function(freq) 3057 | if type(freq) ~= "number" or freq < 0 or freq > sample_rate_half then 3058 | error("valid frequency expected", 2) 3059 | end 3060 | local frequency 3061 | for i=1, frequencies.n do 3062 | frequency = frequencies[i] 3063 | if frequency.freq == freq then 3064 | return frequency.weight 3065 | elseif frequency.freq > freq then 3066 | local frequency_last = frequencies[i-1] 3067 | return (freq - frequency_last.freq) / (frequency.freq - frequency_last.freq) * (frequency.weight - frequency_last.weight) + frequency_last.weight 3068 | end 3069 | end 3070 | end, 3071 | frequency_range_weight = function(freq_min, freq_max) 3072 | if type(freq_min) ~= "number" or freq_min < 0 or freq_min > sample_rate_half or 3073 | type(freq_max) ~= "number" or freq_max < 0 or freq_max > sample_rate_half or 3074 | freq_min > freq_max then 3075 | error("valid frequencies expected", 2) 3076 | end 3077 | local weight_sum, frequency = 0 3078 | for i=1, frequencies.n do 3079 | frequency = frequencies[i] 3080 | if frequency.freq >= freq_min then 3081 | if frequency.freq <= freq_max then 3082 | weight_sum = weight_sum + frequency.weight 3083 | else 3084 | break 3085 | end 3086 | end 3087 | end 3088 | return weight_sum 3089 | end 3090 | } 3091 | end, 3092 | -- Creates font 3093 | create_font = function(family, bold, italic, underline, strikeout, size, xscale, yscale, hspace) 3094 | -- Check arguments 3095 | 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 3096 | (xscale ~= nil and type(xscale) ~= "number") or (yscale ~= nil and type(yscale) ~= "number") or (hspace ~= nil and type(hspace) ~= "number") then 3097 | error("expected family, bold, italic, underline, strikeout, size and optional horizontal & vertical scale and intercharacter space", 2) 3098 | end 3099 | -- Set optional arguments (if not already) 3100 | if not xscale then 3101 | xscale = 1 3102 | end 3103 | if not yscale then 3104 | yscale = 1 3105 | end 3106 | if not hspace then 3107 | hspace = 0 3108 | end 3109 | -- Font scale values for increased size & later downscaling to produce floating point coordinates 3110 | local upscale = FONT_PRECISION 3111 | local downscale = 1 / upscale 3112 | -- Body by operation system 3113 | if ffi.os == "Windows" then 3114 | -- Create device context and set light resources deleter 3115 | local resources_deleter 3116 | local dc = ffi.gc(ffi.C.CreateCompatibleDC(nil), function() resources_deleter() end) 3117 | -- Set context coordinates mapping mode 3118 | ffi.C.SetMapMode(dc, ffi.C.MM_TEXT) 3119 | -- Set context backgrounds to transparent 3120 | ffi.C.SetBkMode(dc, ffi.C.TRANSPARENT) 3121 | -- Convert family from utf8 to utf16 3122 | family = utf8_to_utf16(family) 3123 | if ffi.C.wcslen(family) > 31 then 3124 | error("family name to long", 2) 3125 | end 3126 | -- Create font handle 3127 | local font = ffi.C.CreateFontW( 3128 | size * upscale, -- nHeight 3129 | 0, -- nWidth 3130 | 0, -- nEscapement 3131 | 0, -- nOrientation 3132 | bold and ffi.C.FW_BOLD or ffi.C.FW_NORMAL, -- fnWeight 3133 | italic and 1 or 0, -- fdwItalic 3134 | underline and 1 or 0, --fdwUnderline 3135 | strikeout and 1 or 0, -- fdwStrikeOut 3136 | ffi.C.DEFAULT_CHARSET, -- fdwCharSet 3137 | ffi.C.OUT_TT_PRECIS, -- fdwOutputPrecision 3138 | ffi.C.CLIP_DEFAULT_PRECIS, -- fdwClipPrecision 3139 | ffi.C.ANTIALIASED_QUALITY, -- fdwQuality 3140 | ffi.C.DEFAULT_PITCH + ffi.C.FF_DONTCARE, -- fdwPitchAndFamily 3141 | family 3142 | ) 3143 | -- Set new font to device context 3144 | local old_font = ffi.C.SelectObject(dc, font) 3145 | -- Define light resources deleter 3146 | resources_deleter = function() 3147 | ffi.C.SelectObject(dc, old_font) 3148 | ffi.C.DeleteObject(font) 3149 | ffi.C.DeleteDC(dc) 3150 | end 3151 | -- Return font object 3152 | return { 3153 | -- Get font metrics 3154 | metrics = function() 3155 | -- Get font metrics from device context 3156 | local metrics = ffi.new("TEXTMETRICW[1]") 3157 | ffi.C.GetTextMetricsW(dc, metrics) 3158 | return { 3159 | height = metrics[0].tmHeight * downscale * yscale, 3160 | ascent = metrics[0].tmAscent * downscale * yscale, 3161 | descent = metrics[0].tmDescent * downscale * yscale, 3162 | internal_leading = metrics[0].tmInternalLeading * downscale * yscale, 3163 | external_leading = metrics[0].tmExternalLeading * downscale * yscale 3164 | } 3165 | end, 3166 | -- Get text extents 3167 | text_extents = function(text) 3168 | -- Check argument 3169 | if type(text) ~= "string" then 3170 | error("text expected", 2) 3171 | end 3172 | -- Get utf16 text 3173 | text = utf8_to_utf16(text) 3174 | local text_len = ffi.C.wcslen(text) 3175 | -- Get text extents with this font 3176 | local size = ffi.new("SIZE[1]") 3177 | ffi.C.GetTextExtentPoint32W(dc, text, text_len, size) 3178 | return { 3179 | width = (size[0].cx * downscale + hspace * text_len) * xscale, 3180 | height = size[0].cy * downscale * yscale 3181 | } 3182 | end, 3183 | -- Converts text to ASS shape 3184 | text_to_shape = function(text) 3185 | -- Check argument 3186 | if type(text) ~= "string" then 3187 | error("text expected", 2) 3188 | end 3189 | -- Initialize shape as table 3190 | local shape, shape_n = {}, 0 3191 | -- Get utf16 text 3192 | text = utf8_to_utf16(text) 3193 | local text_len = ffi.C.wcslen(text) 3194 | -- Add path to device context 3195 | if text_len > 8192 then 3196 | error("text too long", 2) 3197 | end 3198 | local char_widths 3199 | if hspace ~= 0 then 3200 | char_widths = ffi.new("INT[?]", text_len) 3201 | local size, space = ffi.new("SIZE[1]"), hspace * upscale 3202 | for i=0, text_len-1 do 3203 | ffi.C.GetTextExtentPoint32W(dc, text+i, 1, size) 3204 | char_widths[i] = size[0].cx + space 3205 | end 3206 | end 3207 | ffi.C.BeginPath(dc) 3208 | ffi.C.ExtTextOutW(dc, 0, 0, 0x0, nil, text, text_len, char_widths) 3209 | ffi.C.EndPath(dc) 3210 | -- Get path data 3211 | local points_n = ffi.C.GetPath(dc, nil, nil, 0) 3212 | if points_n > 0 then 3213 | local points, types = ffi.new("POINT[?]", points_n), ffi.new("BYTE[?]", points_n) 3214 | ffi.C.GetPath(dc, points, types, points_n) 3215 | -- Convert points to shape 3216 | local i, last_type, cur_type, cur_point = 0 3217 | while i < points_n do 3218 | cur_type, cur_point = types[i], points[i] 3219 | if cur_type == ffi.C.PT_MOVETO then 3220 | if last_type ~= ffi.C.PT_MOVETO then 3221 | shape_n = shape_n + 1 3222 | shape[shape_n] = "m" 3223 | last_type = cur_type 3224 | end 3225 | shape[shape_n+1] = Yutils.math.round(cur_point.x * downscale * xscale, FP_PRECISION) 3226 | shape[shape_n+2] = Yutils.math.round(cur_point.y * downscale * yscale, FP_PRECISION) 3227 | shape_n = shape_n + 2 3228 | i = i + 1 3229 | elseif cur_type == ffi.C.PT_LINETO or cur_type == (ffi.C.PT_LINETO + ffi.C.PT_CLOSEFIGURE) then 3230 | if last_type ~= ffi.C.PT_LINETO then 3231 | shape_n = shape_n + 1 3232 | shape[shape_n] = "l" 3233 | last_type = cur_type 3234 | end 3235 | shape[shape_n+1] = Yutils.math.round(cur_point.x * downscale * xscale, FP_PRECISION) 3236 | shape[shape_n+2] = Yutils.math.round(cur_point.y * downscale * yscale, FP_PRECISION) 3237 | shape_n = shape_n + 2 3238 | i = i + 1 3239 | elseif cur_type == ffi.C.PT_BEZIERTO or cur_type == (ffi.C.PT_BEZIERTO + ffi.C.PT_CLOSEFIGURE) then 3240 | if last_type ~= ffi.C.PT_BEZIERTO then 3241 | shape_n = shape_n + 1 3242 | shape[shape_n] = "b" 3243 | last_type = cur_type 3244 | end 3245 | shape[shape_n+1] = Yutils.math.round(cur_point.x * downscale * xscale, FP_PRECISION) 3246 | shape[shape_n+2] = Yutils.math.round(cur_point.y * downscale * yscale, FP_PRECISION) 3247 | shape[shape_n+3] = Yutils.math.round(points[i+1].x * downscale * xscale, FP_PRECISION) 3248 | shape[shape_n+4] = Yutils.math.round(points[i+1].y * downscale * yscale, FP_PRECISION) 3249 | shape[shape_n+5] = Yutils.math.round(points[i+2].x * downscale * xscale, FP_PRECISION) 3250 | shape[shape_n+6] = Yutils.math.round(points[i+2].y * downscale * yscale, FP_PRECISION) 3251 | shape_n = shape_n + 6 3252 | i = i + 3 3253 | else -- invalid type (should never happen, but let us be safe) 3254 | i = i + 1 3255 | end 3256 | if cur_type % 2 == 1 then -- odd = PT_CLOSEFIGURE 3257 | shape_n = shape_n + 1 3258 | shape[shape_n] = "c" 3259 | end 3260 | end 3261 | end 3262 | -- Clear device context path 3263 | ffi.C.AbortPath(dc) 3264 | -- Return shape as string 3265 | return table.concat(shape, " ") 3266 | end 3267 | } 3268 | else -- Unix 3269 | -- Check whether or not the pangocairo library was loaded 3270 | if not pangocairo then 3271 | error("pangocairo library couldn't be loaded", 2) 3272 | end 3273 | -- Create surface, context & layout 3274 | local surface = pangocairo.cairo_image_surface_create(ffi.C.CAIRO_FORMAT_A8, 1, 1) 3275 | local context = pangocairo.cairo_create(surface) 3276 | local layout 3277 | layout = ffi.gc(pangocairo.pango_cairo_create_layout(context), function() 3278 | pangocairo.g_object_unref(layout) 3279 | pangocairo.cairo_destroy(context) 3280 | pangocairo.cairo_surface_destroy(surface) 3281 | end) 3282 | -- Set font to layout 3283 | local font_desc = ffi.gc(pangocairo.pango_font_description_new(), pangocairo.pango_font_description_free) 3284 | pangocairo.pango_font_description_set_family(font_desc, family) 3285 | pangocairo.pango_font_description_set_weight(font_desc, bold and ffi.C.PANGO_WEIGHT_BOLD or ffi.C.PANGO_WEIGHT_NORMAL) 3286 | pangocairo.pango_font_description_set_style(font_desc, italic and ffi.C.PANGO_STYLE_ITALIC or ffi.C.PANGO_STYLE_NORMAL) 3287 | pangocairo.pango_font_description_set_absolute_size(font_desc, size * ffi.C.PANGO_SCALE * upscale) 3288 | pangocairo.pango_layout_set_font_description(layout, font_desc) 3289 | local attr = ffi.gc(pangocairo.pango_attr_list_new(), pangocairo.pango_attr_list_unref) 3290 | pangocairo.pango_attr_list_insert(attr, pangocairo.pango_attr_underline_new(underline and ffi.C.PANGO_UNDERLINE_SINGLE or ffi.C.PANGO_UNDERLINE_NONE)) 3291 | pangocairo.pango_attr_list_insert(attr, pangocairo.pango_attr_strikethrough_new(strikeout)) 3292 | pangocairo.pango_attr_list_insert(attr, pangocairo.pango_attr_letter_spacing_new(hspace * ffi.C.PANGO_SCALE * upscale)) 3293 | pangocairo.pango_layout_set_attributes(layout, attr) 3294 | -- Scale factor for resulting font data 3295 | local fonthack_scale 3296 | if LIBASS_FONTHACK then 3297 | 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) 3298 | fonthack_scale = size / ((pangocairo.pango_font_metrics_get_ascent(metrics) + pangocairo.pango_font_metrics_get_descent(metrics)) / ffi.C.PANGO_SCALE * downscale) 3299 | else 3300 | fonthack_scale = 1 3301 | end 3302 | -- Return font object 3303 | return { 3304 | -- Get font metrics 3305 | metrics = function() 3306 | 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) 3307 | local ascent, descent = pangocairo.pango_font_metrics_get_ascent(metrics) / ffi.C.PANGO_SCALE * downscale, 3308 | pangocairo.pango_font_metrics_get_descent(metrics) / ffi.C.PANGO_SCALE * downscale 3309 | return { 3310 | height = (ascent + descent) * yscale * fonthack_scale, 3311 | ascent = ascent * yscale * fonthack_scale, 3312 | descent = descent * yscale * fonthack_scale, 3313 | internal_leading = 0, 3314 | external_leading = pangocairo.pango_layout_get_spacing(layout) / ffi.C.PANGO_SCALE * downscale * yscale * fonthack_scale 3315 | } 3316 | end, 3317 | -- Get text extents 3318 | text_extents = function(text) 3319 | -- Check argument 3320 | if type(text) ~= "string" then 3321 | error("text expected", 2) 3322 | end 3323 | -- Set text to layout 3324 | pangocairo.pango_layout_set_text(layout, text, -1) 3325 | -- Get text extents with this font 3326 | local rect = ffi.new("PangoRectangle[1]") 3327 | pangocairo.pango_layout_get_pixel_extents(layout, nil, rect) 3328 | return { 3329 | width = rect[0].width * downscale * xscale * fonthack_scale, 3330 | height = rect[0].height * downscale * yscale * fonthack_scale 3331 | } 3332 | end, 3333 | -- Converts text to ASS shape 3334 | text_to_shape = function(text) 3335 | -- Check argument 3336 | if type(text) ~= "string" then 3337 | error("text expected", 2) 3338 | end 3339 | -- Set text path to layout 3340 | pangocairo.cairo_save(context) 3341 | pangocairo.cairo_scale(context, downscale * xscale * fonthack_scale, downscale * yscale * fonthack_scale) 3342 | pangocairo.pango_layout_set_text(layout, text, -1) 3343 | pangocairo.pango_cairo_layout_path(context, layout) 3344 | pangocairo.cairo_restore(context) 3345 | -- Initialize shape as table 3346 | local shape, shape_n = {}, 0 3347 | -- Convert path to shape 3348 | local path = ffi.gc(pangocairo.cairo_copy_path(context), pangocairo.cairo_path_destroy) 3349 | if(path[0].status == ffi.C.CAIRO_STATUS_SUCCESS) then 3350 | local i, cur_type, last_type = 0 3351 | while(i < path[0].num_data) do 3352 | cur_type = path[0].data[i].header.type 3353 | if cur_type == ffi.C.CAIRO_PATH_MOVE_TO then 3354 | if cur_type ~= last_type then 3355 | shape_n = shape_n + 1 3356 | shape[shape_n] = "m" 3357 | end 3358 | shape[shape_n+1] = Yutils.math.round(path[0].data[i+1].point.x, FP_PRECISION) 3359 | shape[shape_n+2] = Yutils.math.round(path[0].data[i+1].point.y, FP_PRECISION) 3360 | shape_n = shape_n + 2 3361 | elseif cur_type == ffi.C.CAIRO_PATH_LINE_TO then 3362 | if cur_type ~= last_type then 3363 | shape_n = shape_n + 1 3364 | shape[shape_n] = "l" 3365 | end 3366 | shape[shape_n+1] = Yutils.math.round(path[0].data[i+1].point.x, FP_PRECISION) 3367 | shape[shape_n+2] = Yutils.math.round(path[0].data[i+1].point.y, FP_PRECISION) 3368 | shape_n = shape_n + 2 3369 | elseif cur_type == ffi.C.CAIRO_PATH_CURVE_TO then 3370 | if cur_type ~= last_type then 3371 | shape_n = shape_n + 1 3372 | shape[shape_n] = "b" 3373 | end 3374 | shape[shape_n+1] = Yutils.math.round(path[0].data[i+1].point.x, FP_PRECISION) 3375 | shape[shape_n+2] = Yutils.math.round(path[0].data[i+1].point.y, FP_PRECISION) 3376 | shape[shape_n+3] = Yutils.math.round(path[0].data[i+2].point.x, FP_PRECISION) 3377 | shape[shape_n+4] = Yutils.math.round(path[0].data[i+2].point.y, FP_PRECISION) 3378 | shape[shape_n+5] = Yutils.math.round(path[0].data[i+3].point.x, FP_PRECISION) 3379 | shape[shape_n+6] = Yutils.math.round(path[0].data[i+3].point.y, FP_PRECISION) 3380 | shape_n = shape_n + 6 3381 | elseif cur_type == ffi.C.CAIRO_PATH_CLOSE_PATH then 3382 | if cur_type ~= last_type then 3383 | shape_n = shape_n + 1 3384 | shape[shape_n] = "c" 3385 | end 3386 | end 3387 | last_type = cur_type 3388 | i = i + path[0].data[i].header.length 3389 | end 3390 | end 3391 | pangocairo.cairo_new_path(context) 3392 | return table.concat(shape, " ") 3393 | end 3394 | } 3395 | end 3396 | end, 3397 | -- Lists available system fonts 3398 | list_fonts = function(with_filenames) 3399 | -- Check argument 3400 | if with_filenames ~= nil and type(with_filenames) ~= "boolean" then 3401 | error("optional boolean expected", 2) 3402 | end 3403 | -- Output fonts buffer 3404 | local fonts = {n = 0} 3405 | -- Body by operation system 3406 | if ffi.os == "Windows" then 3407 | -- Enumerate font families (of all charsets) 3408 | local plogfont = ffi.new("LOGFONTW[1]") 3409 | plogfont[0].lfCharSet = ffi.C.DEFAULT_CHARSET 3410 | plogfont[0].lfFaceName[0] = 0 -- Empty string 3411 | plogfont[0].lfPitchAndFamily = ffi.C.DEFAULT_PITCH + ffi.C.FF_DONTCARE 3412 | local fontname, style, font 3413 | ffi.C.EnumFontFamiliesExW(ffi.gc(ffi.C.CreateCompatibleDC(nil), ffi.C.DeleteDC), plogfont, function(penumlogfont, _, fonttype, _) 3414 | -- Skip different font charsets 3415 | fontname, style = utf16_to_utf8(penumlogfont[0].elfLogFont.lfFaceName), utf16_to_utf8(penumlogfont[0].elfStyle) 3416 | for i=1, fonts.n do 3417 | font = fonts[i] 3418 | if font.name == fontname and font.style == style then 3419 | goto win_font_found 3420 | end 3421 | end 3422 | -- Add font entry 3423 | fonts.n = fonts.n + 1 3424 | fonts[fonts.n] = { 3425 | name = fontname, 3426 | longname = utf16_to_utf8(penumlogfont[0].elfFullName), 3427 | style = style, 3428 | 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", 3429 | } 3430 | ::win_font_found:: 3431 | -- Continue enumeration (till end) 3432 | return 1 3433 | end, 0, 0) 3434 | -- Files to fonts? 3435 | if with_filenames then 3436 | -- Adds filename to fitting font 3437 | local function file_to_font(fontname, fontfile) 3438 | for i=1, fonts.n do 3439 | font = fonts[i] 3440 | 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 3441 | font.file = fontfile 3442 | end 3443 | end 3444 | end 3445 | -- Search registry for font files 3446 | local pregkey, fontfile = ffi.new("HKEY[1]") 3447 | 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 3448 | local regkey = ffi.gc(pregkey[0], advapi.RegCloseKey) 3449 | 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]") 3450 | while true do 3451 | pvalue_name_size[0], pvalue_data_size[0] = ffi.sizeof(value_name) / ffi.sizeof("wchar_t"), ffi.sizeof(value_data) 3452 | if advapi.RegEnumValueW(regkey, value_index, value_name, pvalue_name_size, nil, nil, value_data, pvalue_data_size) ~= ffi.C.ERROR_SUCCESS then 3453 | break 3454 | else 3455 | value_index = value_index + 1 3456 | end 3457 | fontname = utf16_to_utf8(value_name):gsub("(.*) %(.-%)$", "%1", 1) 3458 | fontfile = utf16_to_utf8(ffi.cast("wchar_t*", value_data)) 3459 | file_to_font(fontname, fontfile) 3460 | if fontname:find(" & ") then 3461 | for fontname in fontname:gmatch("(.-) & ") do 3462 | file_to_font(fontname, fontfile) 3463 | end 3464 | file_to_font(fontname:match(".* & (.-)$"), fontfile) 3465 | end 3466 | end 3467 | end 3468 | end 3469 | else -- Unix 3470 | -- Check whether or not the fontconfig library was loaded 3471 | if not fontconfig then 3472 | error("fontconfig library couldn't be loaded", 2) 3473 | end 3474 | -- Get fonts list from fontconfig 3475 | local fontset = ffi.gc(fontconfig.FcFontList(fontconfig.FcInitLoadConfigAndFonts(), 3476 | ffi.gc(fontconfig.FcPatternCreate(), fontconfig.FcPatternDestroy), 3477 | ffi.gc(fontconfig.FcObjectSetBuild("family", "fullname", "style", "outline", with_filenames and "file" or nil, nil), fontconfig.FcObjectSetDestroy)), 3478 | fontconfig.FcFontSetDestroy) 3479 | -- Enumerate fonts 3480 | local font, family, fullname, style, outline, file 3481 | local cstr, cbool = ffi.new("FcChar8*[1]"), ffi.new("FcBool[1]") 3482 | for i=0, fontset[0].nfont-1 do 3483 | -- Get font informations 3484 | font = fontset[0].fonts[i] 3485 | family, fullname, style, outline, file = nil 3486 | if fontconfig.FcPatternGetString(font, "family", 0, cstr) == ffi.C.FcResultMatch then 3487 | family = ffi.string(cstr[0]) 3488 | end 3489 | if fontconfig.FcPatternGetString(font, "fullname", 0, cstr) == ffi.C.FcResultMatch then 3490 | fullname = ffi.string(cstr[0]) 3491 | end 3492 | if fontconfig.FcPatternGetString(font, "style", 0, cstr) == ffi.C.FcResultMatch then 3493 | style = ffi.string(cstr[0]) 3494 | end 3495 | if fontconfig.FcPatternGetBool(font, "outline", 0, cbool) == ffi.C.FcResultMatch then 3496 | outline = cbool[0] 3497 | end 3498 | if fontconfig.FcPatternGetString(font, "file", 0, cstr) == ffi.C.FcResultMatch then 3499 | file = ffi.string(cstr[0]) 3500 | end 3501 | -- Add font entry 3502 | if family and fullname and style and outline then 3503 | fonts.n = fonts.n + 1 3504 | fonts[fonts.n] = { 3505 | name = family, 3506 | longname = fullname, 3507 | style = style, 3508 | type = outline == 0 and "Raster" or "Outline", 3509 | file = file 3510 | } 3511 | end 3512 | end 3513 | end 3514 | -- Order fonts by name & style 3515 | table.sort(fonts, function(font1, font2) 3516 | if font1.name == font2.name then 3517 | return font1.style < font2.style 3518 | else 3519 | return font1.name < font2.name 3520 | end 3521 | end) 3522 | -- Return collected fonts 3523 | return fonts 3524 | end 3525 | } 3526 | } 3527 | 3528 | -- Put library in global scope (if first script argument is true) 3529 | if ({...})[1] then 3530 | _G.Yutils = Yutils 3531 | end 3532 | 3533 | -- Return library to script loader 3534 | return Yutils --------------------------------------------------------------------------------