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 |
38 |
On Unix, pango, cairo and fontconfig is needed for font support (most distributions should have them as system libraries).
39 |
libpng is needed for PNG support of the image reader.
40 |
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 |
1
5
9
13
99 |
2
6
10
14
100 |
3
7
11
15
101 |
4
8
12
16
102 |
103 |
104 |
x0x1
y0x1
z0x1
w0x1
105 |
x0y1
y0y1
z0y1
w0y1
106 |
x0z1
y0z1
z0z1
w0z1
107 |
x0w1
y0w1
z0w1
w0w1
108 |
109 |
110 |
111 | MATRIX = MATRIX.set_data(matrix_fields)
112 | Sets matrix fields. For more, see MATRIX.get_data.
113 |
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 |
198 |
value: unique value in data
199 |
shapes: table with shapes (as strings), covering value in data
200 |
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 |
233 |
x: horizontal position
234 |
y: vertical position
235 |
alpha: opacity in range 0<=x<=255
236 |
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 |
271 |
wrap_style: text line wrapping mode as number
272 |
scaled_border_and_shadow: borders and shadows should be implicated in script-to-frame scale?
273 |
play_res_x: script horizontal resolution
274 |
play_res_y: script vertical resolution
275 |
276 |
277 |
278 | styles = PARSER.styles()
279 | Returns ASS styles as table. Table keys are style names, values are tables with following fields:
280 |
281 |
fontname: font face name
282 |
fontsize: font size
283 |
bold: bold weight?
284 |
italic: italic style?
285 |
underline: underline decoration?
286 |
strikeout: strikeout decoration?
287 |
scale_x: horizontal scale in percent
288 |
scale_y: vertical scale in percent
289 |
spacing: intercharacter spacing
290 |
angle: z-axis rotation angle
291 |
border_style: border is an outline box?
292 |
outline: outline width
293 |
shadow: shadow distance right-down
294 |
alignment: object alignment on screen (see keypad)
295 |
margin_l: margin from left screen border
296 |
margin_r: margin from right screen border
297 |
margin_v: margin from vertical screen borders
298 |
encoding: codepage to interpret text
299 |
300 |
301 |
302 | dialogs = PARSER.dialogs([extended])
303 | Returns ASS dialogs as table. Each entry is a table with following fields:
304 |
305 |
comment: dialog is comment?
306 |
layer: dialog layer number
307 |
start_time: dialog start time in milliseconds
308 |
end_time: dialog end time in milliseconds
309 |
style: dialog style name
310 |
actor: dialog actor name
311 |
margin_l: dialog margin to left edge
312 |
margin_r: dialog margin to right
313 |
margin_v: dialog margin to horizontal edges
314 |
effect: dialog effect description
315 |
text: dialog text
316 |
317 | If extended is true, following additional fields are added:
318 |
319 |
i: dialog index
320 |
duration: dialog duration in milliseconds
321 |
mid_time: dialog mid time in milliseconds
322 |
styleref: reference to dialog related style table
323 |
text_stripped: dialog text without tags
324 |
width: dialog width
325 |
height: dialog height
326 |
ascent: dialog font ascent
327 |
descent: dialog font descent
328 |
internal_leading: dialog font internal leading
329 |
external_leading: dialog font external leading
330 |
left: dialog left position
331 |
center: dialog center position
332 |
right: dialog right position
333 |
x: dialog horizontal position by alignment
334 |
top: dialog top position
335 |
middle: dialog middle position
336 |
bottom: dialog bottom positon
337 |
y: dialog vertical position by alignment
338 |
text_chunked: raw text in tag+text chunks with followings fields:
339 |
tags: content between brackets {} in front of text
340 |
text: text after brackets{}
341 |
342 |
syls: raw text in sylable chunks with following fields:
343 |
i: sylable index
344 |
start_time: sylable start time in milliseconds
345 |
mid_time: sylable mid time in milliseconds
346 |
end_time: sylable end time in milliseconds
347 |
duration: sylable duration in milliseconds
348 |
tags: sylable tags beside her duration definition
349 |
text: sylable text
350 |
prespace: sylable spaces number in front
351 |
postspace: sylable spaces number behind
352 |
width: sylable width
353 |
height: sylable height
354 |
ascent: sylable font ascent
355 |
descent: sylable font descent
356 |
internal_leading: sylable font internal leading
357 |
external_leading: sylable font external leading
358 |
left: sylable left position
359 |
center: sylable center position
360 |
right: sylable right position
361 |
x: sylable horizontal position by alignment
362 |
top: sylable top position
363 |
middle: sylable middle position
364 |
bottom: sylable bottom position
365 |
y: sylable vertical position by alignment
366 |
367 |
words: raw text in word chunks with following fields:
368 |
i: word index
369 |
start_time: word start time in milliseconds
370 |
mid_time: word mid time in milliseconds
371 |
end_time: word end time in milliseconds
372 |
duration: word duration in milliseconds
373 |
text: word text
374 |
prespace: word spaces number in front
375 |
postspace: word spaces number behind
376 |
width: word width
377 |
height: word height
378 |
ascent: word font ascent
379 |
descent: word font descent
380 |
internal_leading: word font internal leading
381 |
external_leading: word font external leading
382 |
left: word left position
383 |
center: word center position
384 |
right: word right position
385 |
x: word horizontal position by alignment
386 |
top: word top position
387 |
middle: word middle position
388 |
bottom: word bottom position
389 |
y: word vertical position by alignment
390 |
391 |
chars: raw text in character chunks with followings fields:
392 |
i: character index
393 |
start_time: character start time in milliseconds
394 |
mid_time: character mid time in milliseconds
395 |
end_time: character end time in milliseconds
396 |
duration: character duration in milliseconds
397 |
text: character text
398 |
syl_i: index of sylable which contains the character
399 |
word_i: index of word which contains the character
400 |
width: character width
401 |
height: character height
402 |
ascent: character font ascent
403 |
descent: character font descent
404 |
internal_leading: character font internal leading
405 |
external_leading: character font external leading
406 |
left: character left position
407 |
center: character center position
408 |
right: character right position
409 |
x: character horizontal position by alignment
410 |
top: character top position
411 |
middle: character middle position
412 |
bottom: character bottom position
413 |
y: character vertical position by alignment
414 |
415 |
leadin: dialog pretime / duration from last dialog to this one (in case there's none, it's 1000.1)
416 |
leadout: dialog posttime / duration from this dialog to next one (in case there's none, it's 1000.1)
417 |
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 |
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 |
461 |
r: red channel strength in range 0<=x<=255
462 |
g: green channel strength in range 0<=x<=255
463 |
b: blue channel strength in range 0<=x<=255
464 |
a: alpha channel strength in range 0<=x<=255
465 |
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 |
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 |
536 |
freq: frequency value
537 |
weight: weight of this frequency in sum of all
538 |
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 |
564 |
ascent: font ascent
565 |
descent: font descent
566 |
internal_leading: font internal leading
567 |
external_leading: font external leading
568 |
height: font maximal height
569 |
570 |
571 |
572 | extents = FONT_HANDLE.text_extents(text)
573 | Returns extents of text with given font as table with followings fields:
574 |
575 |
width: text width
576 |
height: text height
577 |
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 |
588 |
name: short fontname
589 |
longname: full fontname
590 |
style: font style
591 |
type: font storage type
592 |
file: font file (with_filenames must be true; successfull extraction isn't ensured)
593 |
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
--------------------------------------------------------------------------------