diff --git a/src/Rust/Cargo.lock b/src/Rust/Cargo.lock index b0f1d2f5afbda940ff3c2410e16e37e1e932624e..4b7778616095bf320af0a30a40d354fcc966c309 100644 --- a/src/Rust/Cargo.lock +++ b/src/Rust/Cargo.lock @@ -2,20 +2,42 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5110f1c78cf582855d895ecd0746b653db010cec6d9f5575293f27934d980a39" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "autocfg" @@ -52,9 +74,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.2.1" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" +checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ "clap_builder", "clap_derive", @@ -63,10 +85,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.1" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" dependencies = [ + "anstyle", "bitflags", "clap_lex", "strsim", @@ -75,9 +98,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c22dcfb410883764b29953103d9ef7bb8fe21b3fa1158bc99986c2067294bd" +checksum = "1a19591b2ab0e3c04b588a0e04ddde7b9eaa423646d1b4a8092879216bf47473" dependencies = [ "clap", ] @@ -91,7 +114,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.15", ] [[package]] @@ -129,13 +152,13 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "errno" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -188,13 +211,13 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -214,15 +237,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" [[package]] name = "log" @@ -316,6 +339,15 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser", +] + [[package]] name = "paste" version = "1.0.12" @@ -372,9 +404,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -383,9 +415,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "roff" @@ -401,16 +433,16 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.7" +version = "0.37.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -436,9 +468,9 @@ dependencies = [ [[package]] name = "scc" -version = "1.4.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb666a705482e987d4bbd8bb7b0a07438ecfde629044176f0a571605c03e22a" +checksum = "a242e0a9cf55e2fd90e82409ae16c20e45d8c33d1be61cc3d2fe68de0f9ca128" [[package]] name = "scopeguard" @@ -448,9 +480,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" [[package]] name = "serde_spanned" @@ -492,9 +524,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.13" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -508,7 +540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -528,7 +560,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.15", ] [[package]] @@ -566,6 +598,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "ttf-parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dcf002ae3b32cd25400d6df128c5babec3927cd1eb7ce813cfff20eb6c3746" + [[package]] name = "unicode-ident" version = "1.0.8" @@ -605,7 +643,9 @@ dependencies = [ "log", "scc", "thiserror", + "vvs_font", "vvs_procmacro", + "vvs_utils", ] [[package]] @@ -620,8 +660,21 @@ dependencies = [ "log", "scc", "thiserror", + "vvs_font", "vvs_lua", "vvs_repl", + "vvs_utils", +] + +[[package]] +name = "vvs_font" +version = "0.4.0" +dependencies = [ + "ab_glyph", + "lazy_static", + "log", + "thiserror", + "ttf-parser", ] [[package]] @@ -634,6 +687,7 @@ dependencies = [ "thiserror", "toml", "vvs_ass", + "vvs_utils", ] [[package]] @@ -642,7 +696,7 @@ version = "0.4.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.15", ] [[package]] @@ -655,6 +709,10 @@ dependencies = [ "vvs_lua", ] +[[package]] +name = "vvs_utils" +version = "0.4.0" + [[package]] name = "winapi" version = "0.3.9" @@ -677,37 +735,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets", ] [[package]] @@ -716,93 +750,51 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.0" @@ -811,9 +803,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" dependencies = [ "memchr", ] diff --git a/src/Rust/Cargo.toml b/src/Rust/Cargo.toml index 3bd837499ea359181022430d93bb5d62f29c133e..ee090e74309684fe576b537c4875fdb649c8adcf 100644 --- a/src/Rust/Cargo.toml +++ b/src/Rust/Cargo.toml @@ -1,6 +1,14 @@ [workspace] resolver = "2" -members = ["vvs_cli", "vvs_lua", "vvs_ass", "vvs_repl", "vvs_procmacro"] +members = [ + "vvs_cli", + "vvs_lua", + "vvs_ass", + "vvs_repl", + "vvs_font", + "vvs_utils", + "vvs_procmacro", +] [workspace.package] version = "0.4.0" diff --git a/src/Rust/README.md b/src/Rust/README.md index 5946eef2691799125eb32d4937e62dcb3794da20..766bbd0f6c2e6adeaad6707ae7a0f4225635b4d4 100644 --- a/src/Rust/README.md +++ b/src/Rust/README.md @@ -40,3 +40,8 @@ To visualize the dependency graph of VivyScript, use the command: cargo install cargo-depgraph cargo depgraph --build-deps --dedup-transitive-deps | dot -Tpdf | zathura - + +# Licence + +- The VVS project is under the MIT licence +- The NotoSans fonts are distributed using the [OFL licence](utils/fonts/OFL.txt) diff --git a/src/Rust/vvs_ass/Cargo.toml b/src/Rust/vvs_ass/Cargo.toml index d0b222ee62931a1d5bd0c6560d6fe38d9588ba73..95e6f25caa52294ff0a4db5c99b0b2a6c446c304 100644 --- a/src/Rust/vvs_ass/Cargo.toml +++ b/src/Rust/vvs_ass/Cargo.toml @@ -4,10 +4,12 @@ version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true -description.workspace = true +description = "ASS specification and VVS specificities for VVCC" [dependencies] vvs_procmacro = { path = "../vvs_procmacro" } +vvs_utils = { path = "../vvs_utils" } +vvs_font = { path = "../vvs_font" } lazy_static.workspace = true thiserror.workspace = true diff --git a/src/Rust/vvs_ass/src/colors.rs b/src/Rust/vvs_ass/src/colors.rs new file mode 100644 index 0000000000000000000000000000000000000000..71bfd0191e28eaf7d71092f251f0191c998048d1 --- /dev/null +++ b/src/Rust/vvs_ass/src/colors.rs @@ -0,0 +1,179 @@ +use vvs_utils::*; + +/// The color representation +#[derive(Debug, Clone, PartialEq)] +pub enum ASSColor { + /// The Red Green Blue Alpha representation. + RGBA { r: u8, g: u8, b: u8, a: u8 }, + + /// The Hue Saturation Lightness with Alpha representation. The Hue must be between is in + /// radian, the lightness and saturation values must be between 0 and 1. + HSLA { h: f32, s: f32, l: f32, a: u8 }, +} + +macro_rules! rgb { + ($NAME: ident => $r: literal $g: literal $b: literal) => { + pub const $NAME: ASSColor = ASSColor::RGBA { + r: $r, + g: $g, + b: $b, + a: 0, + }; + }; +} + +impl ASSColor { + rgb! { WHITE => 255 255 255 } + rgb! { SILVER => 192 192 192 } + rgb! { GRAY => 128 128 128 } + rgb! { BLACK => 0 0 0 } + + rgb! { RED => 255 0 0 } + rgb! { MAROON => 128 0 0 } + rgb! { YELLOW => 255 255 0 } + rgb! { OLIVE => 128 128 0 } + rgb! { LIME => 0 255 0 } + rgb! { GREEN => 0 128 0 } + rgb! { AQUA => 0 255 255 } + rgb! { TEAL => 0 128 128 } + rgb! { BLUE => 0 0 255 } + rgb! { NAVY => 0 0 128 } + rgb! { FUCHSIA => 255 0 255 } + rgb! { PURPLE => 128 0 128 } + + fn skip_delimiters(str: &str) -> &str { + str.trim() + .trim_start_matches('#') + .trim_start_matches('&') + .trim_end_matches('&') + } + + pub fn try_from_rgba(str: impl AsRef<str>) -> Result<Self, String> { + let color = Self::skip_delimiters(str.as_ref()); + if !(color.len() == 6 || color.len() == 8) { + return Err(format!( + "invalid color string description: {}", + str.as_ref() + )); + } + Ok(Self::RGBA { + r: u8::from_str_radix(&color[0..2], 16) + .map_err(|err| format!("invalid red description: {err}"))?, + g: u8::from_str_radix(&color[2..4], 16) + .map_err(|err| format!("invalid green description: {err}"))?, + b: u8::from_str_radix(&color[4..6], 16) + .map_err(|err| format!("invalid blue description: {err}"))?, + a: (color.len() == 8) + .then(|| { + u8::from_str_radix(&color[6..8], 16) + .map_err(|err| format!("invalid alpha description: {err}")) + }) + .unwrap_or(Ok(0))?, + }) + } + + pub fn try_from_bgra(str: impl AsRef<str>) -> Result<Self, String> { + let color = Self::skip_delimiters(str.as_ref()); + if !(color.len() == 6 || color.len() == 8) { + return Err(format!( + "invalid color string description: {}", + str.as_ref() + )); + } + Ok(Self::RGBA { + r: u8::from_str_radix(&color[4..6], 16) + .map_err(|err| format!("invalid red description: {err}"))?, + g: u8::from_str_radix(&color[2..4], 16) + .map_err(|err| format!("invalid green description: {err}"))?, + b: u8::from_str_radix(&color[0..2], 16) + .map_err(|err| format!("invalid blue description: {err}"))?, + a: (color.len() == 8) + .then(|| { + u8::from_str_radix(&color[6..8], 16) + .map_err(|err| format!("invalid alpha description: {err}")) + }) + .unwrap_or(Ok(0))?, + }) + } + + pub fn into_rgba(self) -> Self { + match self { + this @ ASSColor::RGBA { .. } => this, + ASSColor::HSLA { h, s, l, a } => { + let h = f32_degres_clamp(h); + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + + let (r, g, b) = if (0.0..60.0).contains(&h) { + (c, x, 0.0) + } else if (60.0..120.0).contains(&h) { + (x, c, 0.0) + } else if (120.0..180.0).contains(&h) { + (0.0, c, x) + } else if (180.0..240.0).contains(&h) { + (0.0, x, c) + } else if (240.0..300.0).contains(&h) { + (x, 0.0, c) + } else if (300.0..360.0).contains(&h) { + (c, 0.0, x) + } else { + unreachable!() + }; + + ASSColor::RGBA { + r: ((r + m) * 255.0).trunc() as u8, + g: ((g + m) * 255.0).trunc() as u8, + b: ((b + m) * 255.0).trunc() as u8, + a, + } + } + } + } + + pub fn into_hsla(self) -> Self { + match self { + this @ ASSColor::HSLA { .. } => this, + ASSColor::RGBA { r, g, b, a } => { + const U8_MAX: f32 = u8::MAX as f32; + let h = self.hue(); + let (r, g, b) = (r as f32 / U8_MAX, g as f32 / U8_MAX, b as f32 / U8_MAX); + let (min, max) = minmax_partial!(r, g, b); + let l = (min + max) / 2.0; + let delta = max - min; + let s = either!(f32_epsilon_eq(delta, 0.0) => 0.0; + delta / (1.0 - (2.0 * l - 1.0).abs()) + ); + ASSColor::HSLA { h, s, l, a } + } + } + } + + /// Returns the HUE from the color (the H in HSL/HSV). + fn hue(&self) -> f32 { + match self { + ASSColor::HSLA { h, .. } => *h, + ASSColor::RGBA { r, g, b, .. } => { + let (r, g, b) = (*r, *g, *b); + let (min, max) = minmax_partial!(r, g, b); + let c = max - min; + ((if c == 0 { + 0.0 + } else if max == r { + let segment = (g as f32 - b as f32) / c as f32; + either!(segment < 0.0 => segment + 6.0; segment) + } else if max == g { + let segment = (b as f32 - r as f32) / c as f32; + segment + 2.0 + } else if max == b { + let segment = (r as f32 - g as f32) / c as f32; + segment + 4.0 + } else { + panic!() + } * 60.0) + % 360.0) + .trunc() + } + } + } +} diff --git a/src/Rust/vvs_ass/src/definitions.rs b/src/Rust/vvs_ass/src/definitions.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8b5689f06fca6eefacf811ef4d83523691ab09d --- /dev/null +++ b/src/Rust/vvs_ass/src/definitions.rs @@ -0,0 +1,204 @@ +use std::str::FromStr; + +/// The section in the ASS file. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ASSFileSection { + ScriptInfo, + V4Styles, + Events, +} + +/// The events of the [ASSFileSection::Events] section. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ASSEvent { + /// - Marked=0 means the line is not shown as "marked" in SSA. + /// - Marked=1 means the line is shown as "marked" in SSA. + pub marked: bool, + + /// Subtitles having different layer number will be ignored during the collusion detection. + /// Higher numbered layers will be drawn over the lower numbered. + pub layer: i64, + + /// Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the + /// time elapsed during script playback at which the text will appear onscreen. Note that there + /// is a single digit for the hours! + pub start: i64, + + /// End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time + /// elapsed during script playback at which the text will disappear offscreen. Note that there + /// is a single digit for the hours! + pub end: i64, + + /// Style name. If it is "Default", then your own *Default style will be subtituted. However, + /// the Default style used by the script author IS stored in the script even though SSA ignores + /// it - so if you want to use it, the information is there - you could even change the Name in + /// the Style definition line, so that it will appear in the list of "script" styles. + pub style: String, + + /// Character name. This is the name of the character who speaks the dialogue. It is for + /// information only, to make the script is easier to follow when editing/timing. + pub name: String, + + /// Transition Effect. This is either empty, or contains information for one of the three + /// transition effects implemented in SSA v4.x The effect names are case sensitive and must + /// appear exactly as shown. The effect names do not have quote marks around them. + /// - "Karaoke" means that the text will be successively highlighted one word at a time. + /// Karaoke as an effect type is obsolete. + /// - "Scroll up;y1;y2;delay[;fadeawayheight]"means that the text/picture will scroll up the + /// screen. The parameters after the words "Scroll up" are separated by semicolons. The y1 + /// and y2 values define a vertical region on the screen in which the text will scroll. The + /// values are in pixels, and it doesn't matter which value (top or bottom) comes first. If + /// the values are zeroes then the text will scroll up the full height of the screen. The + /// delay value can be a number from 1 to 100, and it slows down the speed of the scrolling - + /// zero means no delay and the scrolling will be as fast as possible. + /// - "Banner;delay" means that text will be forced into a single line, regardless of length, + /// and scrolled from right to left accross the screen. The delay value can be a number from + /// 1 to 100, and it slows down the speed of the scrolling - zero means no delay and the + /// scrolling will be as fast as possible. + /// - "Scroll down;y1;y2;delay[;fadeawayheight]" + /// - "Banner;delay[;lefttoright;fadeawaywidth]" lefttoright 0 or 1. This field is optional. + /// Default value is 0 to make it backwards compatible. When delay is greater than 0, moving + /// one pixel will take (1000/delay) second. (WARNING: Avery Lee’s "subtitler" plugin reads + /// the "Scroll up" effect parameters as delay;y1;y2) fadeawayheight and fadeawaywidth + /// parameters can be used to make the scrolling text at the sides transparent. + pub effect: String, + + /// Subtitle Text. This is the actual text which will be displayed as a subtitle onscreen. + /// Everything after the 9th comma is treated as the subtitle text, so it can include commas. + /// The text can include \n codes which is a line break, and can include Style Override control + /// codes, which appear between braces { }. + pub text: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ScriptInfoKey { + /// This is a description of the script. If the original author(s) did not provide this + /// information then <untitled> is automatically substituted + Title, + + /// The original author(s) of the script. If the original author(s) did not provide this + /// information then <unknown> is automatically substituted + OriginalScript, + + /// The original translator of the dialogue. This entry does not appear if no information was + /// entered by the author. (optional) + OriginalTranslation, + + /// The original script editor(s), typically whoever took the raw translation and turned it + /// into idiomatic english and reworded for readability. This entry does not appear if no + /// information was entered by the author. (optional) + OriginalEditing, + + /// Whoever timed the original script. This entry does not appear if no information was entered + /// by the author. (optional) + OriginalTiming, + + /// Description of where in the video the script should begin playback. This entry does not + /// appear if no information was entered by the author. (optional) + SynchPoint, + + /// Names of any other subtitling groups who edited the original script. This entry does not + /// appear if subsequent editors did not enter the information. (optional) + ScriptUpdatedBy, + + /// The details of any updates to the original script - made by other subtitling groups. This + /// entry does not appear if subsequent editors did not enter any information + UpdateDetails, + + /// This is the SSA script format version eg. "V4.00". It is used by SSA to give a warning if + /// you are using a version of SSA older than the version that created the script. + /// ***ASS version is "V4.00+"*** + ScriptType, + + /// This determines how subtitles are moved, when automatically preventing onscreen collisions. + /// + /// If the entry says "Normal" then SSA will attempt to position subtitles in the position + /// specified by the "margins". However, subtitles can be shifted vertically to prevent + /// onscreen collisions. With "normal" collision prevention, the subtitles will "stack up" one + /// above the other - but they will always be positioned as close the vertical (bottom) margin + /// as possible - filling in "gaps" in other subtitles if one large enough is available. + /// + /// If the entry says "Reverse" then subtitles will be shifted upwards to make room for + /// subsequent overlapping subtitles. This means the subtitles can nearly always be read + /// top-down - but it also means that the first subtitle can appear half way up the screen + /// before the subsequent overlapping subtitles appear. It can use a lot of screen area. + Collisions, + + /// This is the height of the screen used by the script's author(s) when playing the script. + /// SSA v4 will automatically select the nearest enabled setting, if you are using Directdraw + /// playback + PlayResY, + + /// This is the width of the screen used by the script's author(s) when playing the script. SSA + /// will automatically select the nearest enabled, setting if you are using Directdraw + /// playback + PlayResX, + + /// This is the colour depth used by the script's author(s) when playing the script. SSA will + /// automatically select the nearest enabled setting if you are using Directdraw playback. + PlayDepth, + + /// This is the Timer Speed for the script, as a percentage. eg. "100.0000" is exactly 100%. It + /// has four digits following the decimal point. + /// + /// The timer speed is a time multiplier applied to SSA's clock to stretch or compress the + /// duration of a script. A speed greater than 100% will reduce the overall duration, and means + /// that subtitles will progressively appear sooner and sooner. A speed less than 100% will + /// increase the overall duration of the script means subtitles will progressively appear later + /// and later (like a positive ramp time). + /// + /// The stretching or compressing only occurs during script playback - this value does not + /// change the actual timings for each event listed in the script. + /// + /// Check the SSA user guide if you want to know why "Timer Speed" is more powerful than "Ramp + /// Time", even though they both achieve the same result. + Timer, + + /// Defines the default wrapping style: + /// - 0: smart wrapping, lines are evenly broken + /// - 1: end-of-line word wrapping, only \N breaks + /// - 2: no word wrapping, \n \N both breaks + /// - 3: same as 0, but lower line gets wider. + WrapStyle, +} + +impl FromStr for ScriptInfoKey { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + use ScriptInfoKey::*; + match s.trim() { + "Title" => Ok(Title), + "Original Script" => Ok(OriginalScript), + "Original Translation" => Ok(OriginalTranslation), + "Original Editing" => Ok(OriginalEditing), + "Original Timing" => Ok(OriginalTiming), + "Synch Point" => Ok(SynchPoint), + "Script Updated By" => Ok(ScriptUpdatedBy), + "Update Details" => Ok(UpdateDetails), + "Script Type" => Ok(ScriptType), + "Collisions" => Ok(Collisions), + "PlayResY" => Ok(PlayResY), + "PlayResX" => Ok(PlayResX), + "PlayDepth" => Ok(PlayDepth), + "Timer" => Ok(Timer), + "WrapStyle" => Ok(WrapStyle), + _ => Err(format!("unknown Script Info key: {s}")), + } + } +} + +impl FromStr for ASSFileSection { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + use ASSFileSection::*; + const TRIM_PAT: &[char] = &['[', ']', ' ', '\t']; + match s.trim_matches(TRIM_PAT) { + "Script Info" => Ok(ScriptInfo), + "Events" => Ok(Events), + "v4 Styles" | "v4 Styles+" => Ok(V4Styles), + _ => Err(format!("unknown section [{s}]")), + } + } +} diff --git a/src/Rust/vvs_ass/src/drawing.rs b/src/Rust/vvs_ass/src/drawing.rs new file mode 100644 index 0000000000000000000000000000000000000000..a3c9a997f735de2506a3d614a3bad7eb6bba7097 --- /dev/null +++ b/src/Rust/vvs_ass/src/drawing.rs @@ -0,0 +1,179 @@ +/// Enum used to represent drawing commands. +/// +/// Things you should know: +/// - Commands must appear after {\p1+} and before {\p0}. (except for \clip(..)) +/// - Drawings must always start with a move to command. +/// - Drawings must form a closed shape. +/// - All unclosed shape will be closed with a straight line automatically. +/// - Overlapping shapes in the Dialogue line will be XOR-ed with each-other. +/// - If the same command follows another, it isn’t needed to write its identifier letter again, +/// only the coordinates. +/// - The coordinates are relative to the current cursor position (baseline) and the alignment +/// mode. +/// - Commands p and c should only follow other b-spline commands. +/// +/// Examples: +/// - Square: `m 0 0 l 100 0 100 100 0 100` +/// - Rounded square: `m 0 0 s 100 0 100 100 0 100 c` +/// (c equals to `p 0 0 100 0 100 100` in this case) +/// - Circle (almost): `m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0` +/// (note that the 2nd 'b' is optional here) +#[derive(Debug, Clone, PartialEq)] +pub enum ASSDrawingCmd { + // Moves the cursor to <x>, <y> + M { + x: i64, + y: i64, + }, + + // Moves the cursor to <x>, <y> (unclosed shapes will be left open) + N { + x: i64, + y: i64, + }, + + // Draws a line to <x>, <y> + L { + x: i64, + y: i64, + }, + + // 3rd degree bezier curve to point 3 using point 1 and 2 as the control points + B { + x1: i64, + y1: i64, + x2: i64, + y2: i64, + x3: i64, + y3: i64, + }, + + // 3rd degree uniform b-spline to point N, must contain at least 3 coordinates + S { + x1: i64, + y1: i64, + x2: i64, + y2: i64, + x3: i64, + y3: i64, + others: Vec<(i64, i64)>, + }, + + // Extend b-spline to <x>, <y> + P { + x: i64, + y: i64, + }, + + // close b-spline + C, +} + +/// Contains an ASS drawing. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct ASSDrawing { + content: Vec<ASSDrawingCmd>, +} + +impl ASSDrawingCmd { + pub fn is_move_cmd(&self) -> bool { + use ASSDrawingCmd::*; + matches!(self, M { .. } | N { .. }) + } +} + +impl ASSDrawing { + /// Verify that the drawing is correct! + pub fn verify(&self) -> bool { + match &self.content[..] { + [] => true, + [head] => head.is_move_cmd(), + whole @ [head, content @ ..] if head.is_move_cmd() => { + let mut in_bsplit_line = false; + for (prev, curr) in whole.iter().zip(content) { + match curr { + ASSDrawingCmd::M { .. } | ASSDrawingCmd::N { .. } => { + // FIXME: This is incorrect for N, it could be at the last position, + // check latter if we can find a moar correct version of the + // check function... + log::error!(target: "ass", "found move cmd `{curr:?}` in the middle of a drawing"); + return false; + } + + ASSDrawingCmd::C | ASSDrawingCmd::P { .. } => { + if !matches!(prev, ASSDrawingCmd::S { .. }) { + log::error!(target: "ass", "found C/P cmd not after an S command"); + return false; + } + in_bsplit_line = false; + } + + ASSDrawingCmd::S { .. } => in_bsplit_line = true, + ASSDrawingCmd::B { .. } | ASSDrawingCmd::L { .. } => {} + } + } + !in_bsplit_line + } + _ => false, + } + } +} + +impl std::fmt::Display for ASSDrawingCmd { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ASSDrawingCmd::M { x, y } => write!(f, "m {x} {y}"), + ASSDrawingCmd::N { x, y } => write!(f, "n {x} {y}"), + ASSDrawingCmd::L { x, y } => write!(f, "l {x} {y}"), + ASSDrawingCmd::B { + x1, + y1, + x2, + y2, + x3, + y3, + } => write!(f, "b {x1} {y1} {x2} {y2} {x3} {y3}"), + ASSDrawingCmd::S { + x1, + y1, + x2, + y2, + x3, + y3, + others, + } if others.is_empty() => { + write!(f, "b {x1} {y1} {x2} {y2} {x3} {y3}") + } + ASSDrawingCmd::S { + x1, + y1, + x2, + y2, + x3, + y3, + others, + } => { + let others = others + .iter() + .map(|(x, y)| format!("{x} {y}")) + .collect::<Vec<_>>() + .join(" "); + write!(f, "b {x1} {y1} {x2} {y2} {x3} {y3} {others}") + } + ASSDrawingCmd::P { x, y } => write!(f, "p {x} {y}"), + ASSDrawingCmd::C => write!(f, "c"), + } + } +} + +impl std::fmt::Display for ASSDrawing { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let content = self + .content + .iter() + .map(|cmd| format!("{cmd}")) + .collect::<Vec<_>>() + .join(" "); + write!(f, "{content}") + } +} diff --git a/src/Rust/vvs_ass/src/elements/line.rs b/src/Rust/vvs_ass/src/elements/line.rs index 3ed50825a9de0e190ccdecb86a25125ad950bf2d..f58e003d5f950a24bb401f5fd9a3f4b662e91f9c 100644 --- a/src/Rust/vvs_ass/src/elements/line.rs +++ b/src/Rust/vvs_ass/src/elements/line.rs @@ -1,6 +1,6 @@ use crate::{elements::syllabes::ASSSyllabesPtr, ASSAuxTablePtr, ASSPositionPtr}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ASSLine { pub position: ASSPositionPtr, pub content: ASSSyllabesPtr, @@ -8,3 +8,7 @@ pub struct ASSLine { } pub type ASSLinePtr = crate::Ptr<ASSLine>; + +impl ASSLine { + crate::impl_into_ptr! { ASSLinePtr } +} diff --git a/src/Rust/vvs_ass/src/elements/lines.rs b/src/Rust/vvs_ass/src/elements/lines.rs index 87eabd77f85dd8255d0aac9cb33e5ccc8e5e5c76..cdd10df98b6aa2874c54b5bcc82f4bd5790143cd 100644 --- a/src/Rust/vvs_ass/src/elements/lines.rs +++ b/src/Rust/vvs_ass/src/elements/lines.rs @@ -1,6 +1,6 @@ use crate::{elements::line::ASSLinePtr, ASSAuxTablePtr, ASSPositionPtr}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ASSLines { pub position: ASSPositionPtr, pub content: Vec<ASSLinePtr>, @@ -8,3 +8,7 @@ pub struct ASSLines { } pub type ASSLinesPtr = crate::Ptr<ASSLines>; + +impl ASSLines { + crate::impl_into_ptr! { ASSLinesPtr } +} diff --git a/src/Rust/vvs_ass/src/elements/mod.rs b/src/Rust/vvs_ass/src/elements/mod.rs index b0808c07612abd432d5806be6c128bcfa6d61b21..a411fa4c5543192bab6f28fbe900e2fbbfa90e12 100644 --- a/src/Rust/vvs_ass/src/elements/mod.rs +++ b/src/Rust/vvs_ass/src/elements/mod.rs @@ -3,7 +3,33 @@ mod lines; mod syllabe; mod syllabes; -pub use line::*; -pub use lines::*; -pub use syllabe::*; -pub use syllabes::*; +pub use self::{line::*, lines::*, syllabe::*, syllabes::*}; + +use crate::{definitions::ScriptInfoKey, ASSStyle}; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub struct ASSContainer { + pub lines: ASSLinesPtr, + pub script_info: HashMap<ScriptInfoKey, String>, + pub styles: HashMap<String, ASSStyle>, +} + +pub type ASSContainerPtr = crate::Ptr<ASSContainer>; + +impl ASSContainer { + crate::impl_into_ptr! { ASSContainerPtr } + + /// Create an ASS container from its parts, they must be valide! + pub(crate) fn from_parts( + lines: ASSLinesPtr, + script_info: HashMap<ScriptInfoKey, String>, + styles: HashMap<String, ASSStyle>, + ) -> Self { + Self { + lines, + script_info, + styles, + } + } +} diff --git a/src/Rust/vvs_ass/src/elements/syllabe.rs b/src/Rust/vvs_ass/src/elements/syllabe.rs index 1d412182b0be418bdb1ada8a2f4c365e777da42e..00fa4633805ac044f2ecfcf65b9a857588b2942b 100644 --- a/src/Rust/vvs_ass/src/elements/syllabe.rs +++ b/src/Rust/vvs_ass/src/elements/syllabe.rs @@ -1,6 +1,6 @@ use crate::{ASSAuxTablePtr, ASSPositionPtr}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ASSSyllabe { pub position: ASSPositionPtr, pub content: String, @@ -8,3 +8,7 @@ pub struct ASSSyllabe { } pub type ASSSyllabePtr = crate::Ptr<ASSSyllabe>; + +impl ASSSyllabe { + crate::impl_into_ptr! { ASSSyllabePtr } +} diff --git a/src/Rust/vvs_ass/src/elements/syllabes.rs b/src/Rust/vvs_ass/src/elements/syllabes.rs index f57f8a759a3ba128751e20e2ab6387c5f2c7050e..8a8143d9cf0b2408b538717401fa11c21692815f 100644 --- a/src/Rust/vvs_ass/src/elements/syllabes.rs +++ b/src/Rust/vvs_ass/src/elements/syllabes.rs @@ -1,6 +1,6 @@ use crate::{elements::syllabe::ASSSyllabePtr, ASSAuxTablePtr, ASSPositionPtr}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct ASSSyllabes { pub position: ASSPositionPtr, pub content: Vec<ASSSyllabePtr>, @@ -8,3 +8,7 @@ pub struct ASSSyllabes { } pub type ASSSyllabesPtr = crate::Ptr<ASSSyllabes>; + +impl ASSSyllabes { + crate::impl_into_ptr! { ASSSyllabesPtr } +} diff --git a/src/Rust/vvs_ass/src/lib.rs b/src/Rust/vvs_ass/src/lib.rs index 783ab954e5a80e6e38f2809d5bf962ac62b11591..0e1a05ae0fb3ebbb69ab1c75ac971a4a77027983 100644 --- a/src/Rust/vvs_ass/src/lib.rs +++ b/src/Rust/vvs_ass/src/lib.rs @@ -1,14 +1,20 @@ //! ASS objects for Vivy. +mod colors; +mod definitions; +mod drawing; mod elements; mod position; +mod reader; +mod styles; mod types; mod values; -pub use elements::*; -pub use position::*; -pub use types::*; -pub use values::*; +#[cfg(test)] +mod tests; + +pub use crate::{colors::*, drawing::*, elements::*, position::*, styles::*, types::*, values::*}; +pub use reader::{ass_lines_from_file, ASSElementReaderError}; pub type Ptr<T> = std::rc::Rc<std::cell::RefCell<T>>; @@ -18,3 +24,13 @@ macro_rules! ptr { std::rc::Rc::new(std::cell::RefCell::new($expr)) }; } + +macro_rules! impl_into_ptr { + ($PTR: ident) => { + /// Wrap the struct into a pointer. + pub fn into_ptr(self) -> $PTR { + crate::ptr!(self) + } + }; +} +pub(crate) use impl_into_ptr; diff --git a/src/Rust/vvs_ass/src/position.rs b/src/Rust/vvs_ass/src/position.rs index 7012e97a368c3da5cdf418508d5d8d011700f2be..a75eb9e6b3947994e8236b3c3bc70d9d07e7c00a 100644 --- a/src/Rust/vvs_ass/src/position.rs +++ b/src/Rust/vvs_ass/src/position.rs @@ -1,9 +1,26 @@ -/// The position of an object from the top left corner. The real position depends on the align of -/// the object. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ASSPosition { - pub x: i64, - pub y: i64, +/// The position of an object from the top left corner of the screen. The real position depends on +/// the align of the object. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum ASSPosition { + /// An unspecified position. + #[default] + Unspecified, + + /// A static position. + Pos { x: i64, y: i64 }, + + /// A linear movement. + LinearMove { x1: i64, y1: i64, x2: i64, y2: i64 }, + + /// A linear movement with time step specified. + TimedMove { + x1: i64, + y1: i64, + x2: i64, + y2: i64, + from_ms: i64, + to_ms: i64, + }, } /// The alignement of the object. @@ -37,4 +54,36 @@ pub enum ASSAlign { TR = 9, } +/// Pointer used to store a position, to help with mutability with LUA wrappers. pub type ASSPositionPtr = crate::Ptr<ASSPosition>; + +impl std::str::FromStr for ASSAlign { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.trim() { + "1" => Ok(ASSAlign::BL), + "2" => Ok(ASSAlign::BC), + "3" => Ok(ASSAlign::BR), + "4" => Ok(ASSAlign::CL), + "5" => Ok(ASSAlign::CC), + "6" => Ok(ASSAlign::CR), + "7" => Ok(ASSAlign::TL), + "8" => Ok(ASSAlign::TC), + "9" => Ok(ASSAlign::TR), + s => Err(format!( + "invalid value for ASSAlign, must be in `1..=9`, got: {s}" + )), + } + } +} + +impl From<vvs_font::Point> for ASSPosition { + fn from(vvs_font::Point { x, y }: vvs_font::Point) -> Self { + ASSPosition::Pos { x, y } + } +} + +impl ASSPosition { + crate::impl_into_ptr! { ASSPositionPtr } +} diff --git a/src/Rust/vvs_ass/src/reader/ass.rs b/src/Rust/vvs_ass/src/reader/ass.rs new file mode 100644 index 0000000000000000000000000000000000000000..d024c677029c88102e28dd2eb571690773d781b7 --- /dev/null +++ b/src/Rust/vvs_ass/src/reader/ass.rs @@ -0,0 +1,225 @@ +use crate::{ + definitions::{ASSEvent, ASSFileSection, ScriptInfoKey}, + reader::ASSElementReader, + ASSColor, ASSContainer, ASSElementReaderError, ASSLines, ASSStyle, +}; +use std::collections::HashMap; + +/// Documentation available here: http://www.tcax.org/docs/ass-specs.html or in the `utils/manual` +/// folder from the root of the vvs project. +#[derive(Debug, Default)] +pub struct ASSReader { + section: Option<ASSFileSection>, + script_info: HashMap<ScriptInfoKey, String>, + styles: HashMap<String, ASSStyle>, + events: Vec<ASSEvent>, +} + +fn parse_color(color: &str, name: &str) -> Result<ASSColor, ASSElementReaderError> { + ASSColor::try_from_bgra(color) + .map_err(|err| ASSElementReaderError::Custom(format!("invalid {name} color: {err}"))) +} + +fn parse_boolean(boolean: &str, name: &str) -> Result<bool, ASSElementReaderError> { + match boolean.trim() { + "-1" | "1" | "+1" => Ok(true), + "0" => Ok(false), + boolean => Err(ASSElementReaderError::Custom(format!( + "invalid ass boolean for {name} found: {boolean}" + ))), + } +} + +fn parse_float(float: &str, name: &str) -> Result<f64, ASSElementReaderError> { + float.parse::<f64>().map_err(|err| { + ASSElementReaderError::Custom(format!("invalid float value for {name}: {err}")) + }) +} + +fn parse_int(int: &str, name: &str) -> Result<i64, ASSElementReaderError> { + int.parse::<i64>().map_err(|err| { + ASSElementReaderError::Custom(format!("invalid integer value for {name}: {err}")) + }) +} + +/// Parse dates in the `0:00:00:00` format +fn parse_date(date: &str, name: &str) -> Result<i64, ASSElementReaderError> { + let [h, m, s, c] = &date.split(':').collect::<Vec<_>>()[..] else { + return Err(ASSElementReaderError::Custom(format!("invalid date for {name}: {date}"))) + }; + let check_compnent = |str: &str, compnent: &str, len: usize| { + if str.len() > len { + Err(ASSElementReaderError::Custom(format!( + "invalid date for {name}: {date}" + ))) + } else { + let ret = str.parse::<u16>().map_err(|err| { + ASSElementReaderError::Custom(format!( + "invalid component {compnent} for date {name}: {err}" + )) + })?; + Ok(ret as i64) + } + }; + let (h, m, s, c) = ( + check_compnent(h, "hours", 1)?, + check_compnent(m, "minutes", 2)?, + check_compnent(s, "seconds", 2)?, + check_compnent(c, "centi-seconds", 2)?, + ); + Ok(((h * 60 + m) * 60 + s) * 100 + c) +} + +impl ASSReader { + fn read_script_info(&mut self, line: &str) -> Result<(), ASSElementReaderError> { + let Some((key, value)) = line.split_once(':') else { + return Err(ASSElementReaderError::Custom(format!("invalid script info line: {line}"))) + }; + let key = match key + .trim() + .parse::<ScriptInfoKey>() + .map_err(ASSElementReaderError::Custom)? + { + ScriptInfoKey::ScriptType if value.ne("V4.00+") => { + return Err(ASSElementReaderError::Custom(format!( + "invalid value for key '{key:?}' in script info section: {value}" + ))) + } + ScriptInfoKey::WrapStyle if !("0".."3").contains(&value) => { + return Err(ASSElementReaderError::Custom(format!( + "invalid value for key '{key:?}' in script info section: {value}" + ))) + } + key => key, + }; + match self.script_info.get(&key) { + Some(_) => Err(ASSElementReaderError::Custom(format!( + "redefinition of key '{key:?}' in script info section" + ))), + None => { + self.script_info.insert(key, value.trim().to_string()); + Ok(()) + } + } + } + + fn read_v4_style(&mut self, line: &str) -> Result<(), ASSElementReaderError> { + let fields: Vec<_> = line.trim().split(',').map(|str| str.trim()).collect(); + let [_, name, font_name, font_size, primary_color, secondary_color, outline_color, back_color, bold, italic, underline, strikeout, scalex, scaley, spacing, angle, border_style, outline, shadow, alignment, marginl, marginr, marginv, encoding] = &fields[..] else { + return Err(ASSElementReaderError::Custom(format!("invalid line composed of fields: {fields:#?}"))) + }; + + if encoding.ne(&"0") { + return Err(ASSElementReaderError::Custom(format!( + "we expected the encoding '0', got: {encoding}" + ))); + } + + let style = ASSStyle { + name: name.to_string(), + font_name: font_name.to_string(), + font_size: parse_int(font_size, "font size")?, + primary_color: parse_color(primary_color, "primary")?, + secondary_color: parse_color(secondary_color, "secondary")?, + outline_color: parse_color(outline_color, "outline")?, + back_color: parse_color(back_color, "back")?, + bold: parse_boolean(bold, "bold")?, + italic: parse_boolean(italic, "italic")?, + underline: parse_boolean(underline, "underline")?, + strikeout: parse_boolean(strikeout, "strikeout")?, + scale_x: parse_float(scalex, "scale_x")?, + scale_y: parse_float(scaley, "scale_y")?, + spacing: parse_float(spacing, "spacing")?, + angle: parse_float(angle, "angle")?, + border_style: border_style + .parse() + .map_err(ASSElementReaderError::Custom)?, + outline: parse_float(outline, "outline")?, + shadow: parse_float(shadow, "shadow")?, + alignment: alignment.parse().map_err(ASSElementReaderError::Custom)?, + margin_l: parse_int(marginl, "margin_l")?, + margin_r: parse_int(marginr, "margin_r")?, + margin_v: parse_int(marginv, "margin_v")?, + }; + + match self.styles.insert(name.to_string(), style) { + None => Ok(()), + Some(old) => { + log::error!(target: "ass", "redefine style '{name}', previous style was: {old:#?}"); + Err(ASSElementReaderError::Custom(format!( + "redefinition of style '{name}'" + ))) + } + } + } + + fn read_event(&mut self, line: &str) -> Result<(), ASSElementReaderError> { + let fields: Vec<_> = line.trim().splitn(8, ',').map(|str| str.trim()).collect(); + let [marked, layer, start, end, style, name, effect, text] = &fields[..] else { + return Err(ASSElementReaderError::Custom(format!("invalid line composed of fields: {fields:#?}"))) + }; + self.events.push(ASSEvent { + marked: parse_boolean(marked, "marked")?, + layer: parse_int(layer, "layer")?, + start: parse_date(start, "start")?, + end: parse_date(end, "end")?, + style: style.to_string(), + name: name.to_string(), + effect: effect.to_string(), + text: text.to_string(), + }); + Ok(()) + } +} + +impl ASSElementReader for ASSReader { + fn try_read( + mut self, + file: impl std::io::BufRead, + ) -> Result<crate::ASSContainerPtr, ASSElementReaderError> { + // Parse the file + for line in file.lines() { + let line = line.map_err(ASSElementReaderError::FailedToReadLine)?; + let line = line.trim(); + + if line.is_empty() || line.starts_with(';') { + continue; + } else if line.starts_with('[') && line.ends_with(']') { + self.section = Some( + line.parse::<ASSFileSection>() + .map_err(ASSElementReaderError::Custom)?, + ); + } else { + match self.section { + Some(ASSFileSection::ScriptInfo) => self.read_script_info(line)?, + Some(ASSFileSection::V4Styles) => self.read_v4_style(line)?, + Some(ASSFileSection::Events) => self.read_event(line)?, + None => { + return Err(ASSElementReaderError::Custom(format!( + "found the following line without a section: {line}" + ))) + } + } + } + } + + // Verify integrity + order events + self.events.sort_by(|a, b| Ord::cmp(&a.start, &b.end)); + if !self.styles.contains_key(ASSStyle::default_name()) { + self.styles + .insert(ASSStyle::default_name_string(), ASSStyle::default()); + } + for ASSEvent { style, .. } in &mut self.events { + if !self.styles.contains_key(style) { + *style = ASSStyle::default_name_string() + } + } + + Ok(ASSContainer::from_parts( + ASSLines::default().into_ptr(), + self.script_info, + self.styles, + ) + .into_ptr()) + } +} diff --git a/src/Rust/vvs_ass/src/reader/json.rs b/src/Rust/vvs_ass/src/reader/json.rs new file mode 100644 index 0000000000000000000000000000000000000000..e972d937125a49db1ed08d50a3768f3ffd4c02e9 --- /dev/null +++ b/src/Rust/vvs_ass/src/reader/json.rs @@ -0,0 +1,15 @@ +use crate::{reader::ASSElementReader, ASSElementReaderError}; + +/// Documentation available here: http://www.tcax.org/docs/ass-specs.html or in the `utils/manual` +/// folder. +#[derive(Debug, Default)] +pub struct JSONReader {} + +impl ASSElementReader for JSONReader { + fn try_read( + self, + _file: impl std::io::BufRead, + ) -> Result<crate::ASSContainerPtr, ASSElementReaderError> { + todo!() + } +} diff --git a/src/Rust/vvs_ass/src/reader/mod.rs b/src/Rust/vvs_ass/src/reader/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb96abcb41f1fe445daddf3b618c5f23164f9348 --- /dev/null +++ b/src/Rust/vvs_ass/src/reader/mod.rs @@ -0,0 +1,58 @@ +//! Read the content of an ASS file / a Vivy subtitle file and creates an +//! [vvs_ass::elements::lines::ASSLinesPtr] structure accordingly. + +use crate::ASSContainerPtr; +use std::{ + fs::File, + io::{BufReader, Error as IoError}, + path::{Path, PathBuf}, +}; +use thiserror::Error; + +mod ass; +mod json; + +#[derive(Debug, Error)] +pub enum ASSElementReaderError { + #[error("file has no extension: {0}")] + NoExtension(PathBuf), + + #[error("failed to open file {0}: {1}")] + FailedToOpenFile(PathBuf, IoError), + + #[error("unknown file extension for subtitles")] + UnknownExtension(String), + + #[error("failed to read line: {0}")] + FailedToReadLine(IoError), + + #[error("{0}")] + Custom(String), +} + +trait ASSElementReader { + fn try_read( + self, + file: impl std::io::BufRead, + ) -> Result<ASSContainerPtr, ASSElementReaderError>; +} + +pub fn ass_lines_from_file( + file: impl AsRef<Path>, +) -> Result<ASSContainerPtr, ASSElementReaderError> { + let file = file.as_ref(); + let Some(extension) = file.extension() else { + return Err(ASSElementReaderError::NoExtension(file.to_path_buf())); + }; + let content = BufReader::new( + File::open(file) + .map_err(|err| ASSElementReaderError::FailedToOpenFile(file.to_path_buf(), err))?, + ); + match &extension.to_string_lossy()[..] { + "ass" => ass::ASSReader::default().try_read(content), + "vvsb" | "json" => json::JSONReader::default().try_read(content), + extension => Err(ASSElementReaderError::UnknownExtension( + extension.to_string(), + )), + } +} diff --git a/src/Rust/vvs_ass/src/styles.rs b/src/Rust/vvs_ass/src/styles.rs new file mode 100644 index 0000000000000000000000000000000000000000..9b2a8ea763e285701f1f8df3714abd7ed7e7b814 --- /dev/null +++ b/src/Rust/vvs_ass/src/styles.rs @@ -0,0 +1,152 @@ +use crate::{ASSAlign, ASSColor}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum ASSBorderStyle { + #[default] + OutlineAndDropShadow = 1, + OpaqueBox = 3, +} + +/// Type used to describe an ASS style. +#[derive(Debug, Clone, PartialEq)] +pub struct ASSStyle { + /// The name of the Style. Case sensitive. Cannot include commas. + pub name: String, + + /// The fontname as used by libass. Case-sensitive. Can't include comas. + pub font_name: String, + + /// The size of the font. Don't use a thing that is too big... Must be positive. + pub font_size: i64, + + /// The colour that a subtitle will normally appear in. + pub primary_color: ASSColor, + + /// This colour may be used instead of the Primary colour when a subtitle is automatically + /// shifted to prevent an onscreen collsion, to distinguish the different subtitles. + pub secondary_color: ASSColor, + + /// Color of the outline of each characters. + pub outline_color: ASSColor, + + /// This is the colour of the subtitle outline or shadow, if these are used. + pub back_color: ASSColor, + + /// Whever the font is in bold. + pub bold: bool, + + /// Whever the font is in italic. + pub italic: bool, + + /// Whever the font is underlined. + pub underline: bool, + + /// Whever the font is strikeout. + pub strikeout: bool, + + /// Modifies the width of the font. Value in percent. + pub scale_x: f64, + + /// Modifies the height of the font. Value in percent. + pub scale_y: f64, + + /// Extra space between characters. Value in pixels. + pub spacing: f64, + + /// The origin of the rotation is defined by the alignment. Can be a floating point number. + /// Value in degrees. + pub angle: f64, + + /// The border style. + pub border_style: ASSBorderStyle, + + /// If border_style is [ASSBorderStyle::OutlineAndDropShadow], then this specifies the width of + /// the outline around the text, in pixels. + /// + /// Values may be 0, 1, 2, 3 or 4 the documentation says, but libass seems to support floating + /// values. + pub outline: f64, + + /// If border_style is [ASSBorderStyle::OutlineAndDropShadow], then this specifies the depth of + /// the drop shadow behind the text, in pixels. + /// + /// Values may be 0, 1, 2, 3 or 4 the documentation says, but libass seems to support floating + /// values. Drop shadow is always used in addition to an outline - SSA will force an outline of + /// 1 pixel if no outline width is given. + pub shadow: f64, + + /// This sets how text is "justified" within the Left/Right onscreen margins, and also the + /// vertical placing. + pub alignment: ASSAlign, + + /// This defines the Left Margin in pixels. It is the distance from the left-hand edge of the + /// screen.The three onscreen margins (margin_l, margin_r, margin_v) define areas in which the + /// subtitle text will be displayed. + /// + /// Must be a positive integer. + pub margin_l: i64, + + /// This defines the Right Margin in pixels. It is the distance from the right-hand edge of the + /// screen. The three onscreen margins (margin_l, margin_r, margin_v) define areas in which the + /// subtitle text will be displayed. + pub margin_r: i64, + + /// This defines the vertical Left Margin in pixels. + /// - For a subtitle, it is the distance from the bottom of the screen. + /// - For a toptitle, it is the distance from the top of the screen. + /// - For a midtitle, the value is ignored - the text will be vertically centred + pub margin_v: i64, +} + +impl std::str::FromStr for ASSBorderStyle { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.trim() { + "1" => Ok(ASSBorderStyle::OutlineAndDropShadow), + "3" => Ok(ASSBorderStyle::OpaqueBox), + s => Err(format!( + "invalid value '{s}' for ASSBorderStyle, must be 1 or 3" + )), + } + } +} + +impl ASSStyle { + pub fn default_name() -> &'static str { + "Default" + } + + pub fn default_name_string() -> String { + Self::default_name().to_string() + } +} + +impl Default for ASSStyle { + fn default() -> Self { + Self { + name: Self::default_name_string(), + font_name: Default::default(), + font_size: 14, + primary_color: ASSColor::WHITE, + secondary_color: ASSColor::RED, + outline_color: ASSColor::BLACK, + back_color: ASSColor::BLACK, + bold: false, + italic: false, + underline: false, + strikeout: false, + scale_x: 100.0, + scale_y: 100.0, + spacing: 0.0, + angle: 0.0, + border_style: Default::default(), + outline: 3.0, + shadow: 1.0, + alignment: ASSAlign::TC, + margin_l: 0, + margin_r: 0, + margin_v: 10, + } + } +} diff --git a/src/Rust/vvs_ass/src/tests.rs b/src/Rust/vvs_ass/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..e26c65d260269877ed06465d023552f47e650fa8 --- /dev/null +++ b/src/Rust/vvs_ass/src/tests.rs @@ -0,0 +1,77 @@ +mod color { + use crate::*; + + // https://en.wikipedia.org/wiki/HSL_and_HSV#Hue_and_chroma + const RGB2HSL_TABLE: [(&str, f32, f32, f32); 19] = [ + ("FFFFFF", 0.000, 0.000, 1.000), + ("808080", 0.000, 0.000, 0.500), + ("000000", 0.000, 0.000, 0.000), + ("FF0000", 0.000, 1.000, 0.500), + ("BFBF00", 60.00, 1.000, 0.375), + ("008000", 120.0, 1.000, 0.250), + ("80FFFF", 180.0, 1.000, 0.750), + ("8080FF", 240.0, 1.000, 0.750), + ("BF40BF", 300.0, 0.500, 0.500), + ("A0A424", 61.80, 0.638, 0.393), + ("411BEA", 251.1, 0.832, 0.511), + ("1EAC41", 134.9, 0.707, 0.396), + ("F0C80E", 49.50, 0.893, 0.497), + ("B430E5", 283.7, 0.775, 0.542), + ("ED7651", 14.30, 0.817, 0.624), + ("FEF888", 56.90, 0.991, 0.765), + ("19CB97", 162.4, 0.779, 0.447), + ("362698", 248.3, 0.601, 0.373), + ("7E7EB8", 240.5, 0.290, 0.607), + ]; + + #[test] + fn test_from_string() { + assert!(ASSColor::try_from_rgba("#&AABBCC&").is_ok()); + assert!(ASSColor::try_from_rgba("#AABBCC").is_ok()); + assert!(ASSColor::try_from_rgba("&AABBCC&").is_ok()); + assert!(ASSColor::try_from_rgba("AABBCC").is_ok()); + assert!(ASSColor::try_from_rgba("AABBCCAA").is_ok()); + + assert_eq!( + ASSColor::try_from_rgba("AABBCC").unwrap(), + ASSColor::try_from_bgra("CCBBAA").unwrap() + ); + } + + #[test] + fn test_rgb2hsl() { + for (rgb, h_target, s1_target, l1_target) in RGB2HSL_TABLE { + const EPSILON_DEG: f32 = 1.0; + const EPSILON_0_1: f32 = 10E-3; + let ASSColor::HSLA { h, s, l, .. } = ASSColor::try_from_rgba(rgb).unwrap().into_hsla() else { unreachable!() }; + assert!( + (h_target - h).abs() <= EPSILON_DEG, + "invalid convertion for color #{rgb}, hue {h_target} != {h}" + ); + assert!( + (s - s1_target).abs() <= EPSILON_0_1, + "invalid convertion for color #{rgb}, s {s1_target} != {s}" + ); + assert!( + (l - l1_target).abs() <= EPSILON_0_1, + "invalid convertion for color #{rgb}, l {l1_target} != {l}" + ); + } + } + + #[test] + fn test_hsl2rgb() { + for (rgb, h, s, l) in RGB2HSL_TABLE { + macro_rules! eq { + ($a: expr, $b: expr, $($msg: expr),+) => { + assert!(($a as f32 - $b as f32).abs() <= 1.0, $($msg),+); + }; + } + let ASSColor::RGBA { r, g, b, .. } = ASSColor::HSLA { h, s, l, a: 0 }.into_rgba() else { unreachable!() }; + let ASSColor::RGBA { r: r_target, g: g_target, b: b_target, .. } = ASSColor::try_from_rgba(rgb).unwrap() else { unreachable!() }; + eq! { r_target, r, "invalid convertion on red for #{rgb}: {r_target} != {r}"} + eq! { g_target, g, "invalid convertion on green for #{rgb}: {g_target} != {g}"} + eq! { b_target, b, "invalid convertion on blue for #{rgb}: {b_target} != {b}"} + } + } +} diff --git a/src/Rust/vvs_ass/src/types.rs b/src/Rust/vvs_ass/src/types.rs index 6d6928b2cf936f6be710cad1526b7e5569208ee9..dedd4a63ae51af47a7009c0b4edd7649ef61bcb4 100644 --- a/src/Rust/vvs_ass/src/types.rs +++ b/src/Rust/vvs_ass/src/types.rs @@ -40,6 +40,7 @@ pub enum ASSType { } impl ASSType { + /// Get the name of the ASS type. pub fn as_str(&self) -> &'static str { match self { ASSType::Lines => "lines", @@ -49,6 +50,7 @@ impl ASSType { } } + /// Get the name of the type, but padded with spaces. pub fn as_padded_str(&self) -> &'static str { match self { ASSType::Lines => "lines ", @@ -57,6 +59,14 @@ impl ASSType { ASSType::Syllabe => "syllabe ", } } + + /// Get the base type of the ASS type + pub fn base_type(&self) -> Self { + match self { + ASSType::Lines | ASSType::Line => ASSType::Line, + ASSType::Syllabes | ASSType::Syllabe => ASSType::Syllabe, + } + } } impl AsRef<str> for ASSType { diff --git a/src/Rust/vvs_ass/src/values.rs b/src/Rust/vvs_ass/src/values.rs index 7c656b477963aac846ffb1b7e00fcbfa0e4b444b..1dce8df853975ed9d446ee4c2371e5ffa13137e9 100644 --- a/src/Rust/vvs_ass/src/values.rs +++ b/src/Rust/vvs_ass/src/values.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashMap, convert::TryFrom, rc::Rc}; +use std::{collections::HashMap, convert::TryFrom}; /// The values that can be added to an ASS element. #[derive(Debug, Clone, PartialEq)] @@ -13,7 +13,8 @@ pub enum ASSAuxValue { #[derive(Debug, Default, Clone, PartialEq)] pub struct ASSAuxTable(HashMap<String, ASSAuxValue>); -pub type ASSAuxTablePtr = Rc<RefCell<ASSAuxTable>>; +/// A pointer type for the [ASSAuxTable] type, for easy wrapping +pub type ASSAuxTablePtr = crate::Ptr<ASSAuxTable>; impl ASSAuxValue { pub fn type_str(&self) -> &'static str { @@ -61,7 +62,7 @@ impl std::fmt::Display for ASSAuxValue { ASSAuxValue::Floating(val) => write!(f, "{val}"), ASSAuxValue::Integer(val) => write!(f, "{val}"), ASSAuxValue::Boolean(val) => write!(f, "{val}"), - ASSAuxValue::String(val) => f.write_str(&val), + ASSAuxValue::String(val) => f.write_str(val), } } } diff --git a/src/Rust/vvs_cli/Cargo.toml b/src/Rust/vvs_cli/Cargo.toml index 2a77634e309b0a1bcfe0b9a2ebd475071082de90..e7656212be8817a4789ee2ece2c116dae27fa8ae 100644 --- a/src/Rust/vvs_cli/Cargo.toml +++ b/src/Rust/vvs_cli/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true -description.workspace = true +description = "The CLI for VVS (VVCC)" [[bin]] name = "vvcc" @@ -13,6 +13,8 @@ path = "src/main.rs" [dependencies] vvs_lua = { path = "../vvs_lua" } vvs_repl = { path = "../vvs_repl" } +vvs_font = { path = "../vvs_font" } +vvs_utils = { path = "../vvs_utils" } lazy_static.workspace = true thiserror.workspace = true diff --git a/src/Rust/vvs_cli/src/args.rs b/src/Rust/vvs_cli/src/args.rs index 08c0161b8a33c4d22a9c8102012e905f1ba19529..26fd7ad1bc81c037f52ed45aac096adf6b723cba 100644 --- a/src/Rust/vvs_cli/src/args.rs +++ b/src/Rust/vvs_cli/src/args.rs @@ -8,10 +8,10 @@ use std::path::PathBuf; , version , about , name = "vvcc" - , group = clap::ArgGroup::new("action").args(["manpage", "shell", "script.vvs"]) - , group = clap::ArgGroup::new("repl") .args(["interactive"]) .conflicts_with_all(["shell", "manpage"]) - , group = clap::ArgGroup::new("opts") .args(["options.toml"]).conflicts_with_all(["shell", "manpage"]) - , group = clap::ArgGroup::new("infos") .args(["info"]) .conflicts_with_all(["shell", "manpage"]) + , group = clap::ArgGroup::new("action").args(["manpage", "shell", "font-file", "script.vvs"]) + , group = clap::ArgGroup::new("repl") .args(["interactive"]) .conflicts_with_all(["shell", "manpage", "font-file"]) + , group = clap::ArgGroup::new("opts") .args(["options.toml"]).conflicts_with_all(["shell", "manpage", "font-file"]) + , group = clap::ArgGroup::new("infos") .args(["info"]) .conflicts_with_all(["shell", "manpage", "font-file"]) )] pub struct Args { /// The script to run. @@ -57,6 +57,13 @@ pub struct Args { )] pub info: bool, + /// Display infos about embeded fonts or external fonts. + #[arg( long = "font-info" + , action = clap::ArgAction::Set + , id = "font-file" + )] + pub font_info: Option<Option<PathBuf>>, + /// Make vvcc more verbose, repeat to make it even more verbose #[arg( long , short = 'v' diff --git a/src/Rust/vvs_cli/src/main.rs b/src/Rust/vvs_cli/src/main.rs index 3a20be9f72d84d7a9f146c7868f536e89b6379d5..72abf27bb0f53bf7bae23455f5edfaa6a0b8d168 100644 --- a/src/Rust/vvs_cli/src/main.rs +++ b/src/Rust/vvs_cli/src/main.rs @@ -3,6 +3,29 @@ use anyhow::{Context, Result}; use vvs_cli::{args, logger}; +fn print_face_info(name: &str, font: &[u8]) -> Result<()> { + let font = + vvs_font::Font::try_from(font).with_context(|| format!("failed to parse font {name}"))?; + + let font_types = font + .font_types() + .into_iter() + .map(|ty| format!("{ty:?}")) + .collect::<Vec<_>>() + .join(", "); + + println!("###"); + println!( + "# Family Name: {}", + font.name().with_context(|| "failed to get the font name")? + ); + println!("# Name(s): {}", font.family_names().join(", ")); + println!("# Font Type(s): {font_types}"); + println!("# Number of glyph(s): {}", font.number_of_glyphs()); + + Ok(()) +} + fn main() -> Result<()> { logger::init(None).map_err(Box::new)?; logger::ignore_target("rustyline"); @@ -14,6 +37,7 @@ fn main() -> Result<()> { shell, manpage, info, + font_info, } = <args::Args as clap::Parser>::parse(); logger::level(verbose); @@ -30,6 +54,20 @@ fn main() -> Result<()> { let command_name = command.get_name().to_string(); clap_complete::generate(shell, &mut command, command_name, &mut std::io::stdout()); return Ok(()); + } else if let Some(font_info) = font_info { + match font_info { + Some(path) => { + let font = std::fs::read(&path) + .with_context(|| format!("failed to read font: {}", path.to_string_lossy()))?; + print_face_info(&path.to_string_lossy(), &font)?; + } + None => { + for (name, font) in vvs_font::embeded_fonts() { + print_face_info(name, font)?; + } + } + } + return Ok(()); } if script.is_none() && !interactive { @@ -54,6 +92,11 @@ fn main() -> Result<()> { None => "failed to setup base runtime".to_string(), })?; + if info { + log::debug!(target: "vvcc", "print informations about loaded scripts"); + vvs_lua::print_info(&lua).with_context(|| "failed to print vivy informations")?; + } + vvs_lua::load_user_script(&lua, script.as_ref()).with_context(|| match script { Some(script) => format!( "failed to finish runtime setup and/or load script: {}", @@ -62,11 +105,6 @@ fn main() -> Result<()> { None => "failed to finish runtime setup".to_string(), })?; - if info { - log::debug!(target: "vvcc", "print informations about loaded scripts"); - vvs_lua::print_info(&lua).with_context(|| "failed to print vivy informations")?; - } - if interactive { log::debug!(target: "vvcc", "switch to interactive mode"); vvs_repl::REPL::new(lua) diff --git a/src/Rust/vvs_font/Cargo.toml b/src/Rust/vvs_font/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f1cd7653fbd862f21fff49f5757edb6aa556af87 --- /dev/null +++ b/src/Rust/vvs_font/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "vvs_font" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "The font crate for VVS" + +[dependencies] +lazy_static.workspace = true +thiserror.workspace = true +log.workspace = true + +ttf-parser = { version = "^0.19" } +ab_glyph = { version = "^0.2.20" } diff --git a/src/Rust/vvs_font/build.rs b/src/Rust/vvs_font/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..0ae1bc9de1fee56eb90ecb4607291e6292f70175 --- /dev/null +++ b/src/Rust/vvs_font/build.rs @@ -0,0 +1,57 @@ +use std::{env, fs, path::Path}; + +fn rerun_directory<T: AsRef<Path> + ?Sized>(dir: &T) { + println!("cargo:rerun-if-changed={}", dir.as_ref().to_string_lossy()); + for entry in std::fs::read_dir(dir).unwrap() { + let path = entry.expect("Couldn't access file in src directory").path(); + if path.is_dir() { + rerun_directory(&path); + } + } +} + +fn main() { + let out_dir = Path::new(&env::var_os("OUT_DIR").expect("no OUT_DIR env variable...")) + .canonicalize() + .expect("failed to canonicalize OUT_DIR"); + let font_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("manifest folder should have a parent") + .join("vvs_font/fonts") + .canonicalize() + .expect("failed to canonicalize the font folder"); + + let fonts = fs::read_dir(&font_dir) + .expect("failed to read the font folder") + .filter_map(Result::ok) + .filter(|file| { + file.file_type().map(|ft| ft.is_file()).unwrap_or_default() + && file.path().extension().map(|e| e == "ttf").unwrap_or(false) + }) + .map(|file| { + let (path, file_name) = (file.path(), file.file_name()); + let path = path.to_string_lossy(); + let name = file_name.to_string_lossy(); + let name = name.rsplit_once('.').unwrap().0; + format!("({name:?}, include_bytes!({path:?}))") + }) + .collect::<Vec<_>>() + .join(",\n"); + + // Generate + let src_content = format!( + r#" +pub const fn embeded_fonts() -> &'static [(&'static str, &'static [u8])] {{ + &[ {fonts} ] +}} + "# + ); + + // Write + fs::write(out_dir.join("generated_font_utils.rs"), src_content) + .expect("failed to write generated source file"); + + // Rerun + rerun_directory(&font_dir); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/src/Rust/vvs_font/fonts/NotoSans-Bold.ttf b/src/Rust/vvs_font/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3e68bc24167c9d26e2c982807c8b6f1e6c8047fc Binary files /dev/null and b/src/Rust/vvs_font/fonts/NotoSans-Bold.ttf differ diff --git a/src/Rust/vvs_font/fonts/NotoSans-BoldItalic.ttf b/src/Rust/vvs_font/fonts/NotoSans-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4b5635171654dfda7d30b856357394aad5084ea9 Binary files /dev/null and b/src/Rust/vvs_font/fonts/NotoSans-BoldItalic.ttf differ diff --git a/src/Rust/vvs_font/fonts/NotoSans-Italic.ttf b/src/Rust/vvs_font/fonts/NotoSans-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..eedc5e4593d3eef68109d35823560d98031a7e83 Binary files /dev/null and b/src/Rust/vvs_font/fonts/NotoSans-Italic.ttf differ diff --git a/src/Rust/vvs_font/fonts/NotoSans-Regular.ttf b/src/Rust/vvs_font/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..973bc2ed3a92372004f5bc0c3fe85092c75624e0 Binary files /dev/null and b/src/Rust/vvs_font/fonts/NotoSans-Regular.ttf differ diff --git a/src/Rust/vvs_font/fonts/OFL.txt b/src/Rust/vvs_font/fonts/OFL.txt new file mode 100644 index 0000000000000000000000000000000000000000..90b733268379b957b2aa1990f4e3795ebe55a148 --- /dev/null +++ b/src/Rust/vvs_font/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2015-2021 Google LLC. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/Rust/vvs_font/src/error.rs b/src/Rust/vvs_font/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..00d4c894cf293e8b0f1b97540596a9975d526ba7 --- /dev/null +++ b/src/Rust/vvs_font/src/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum FontCreationError { + #[error("ab_glyph error: {0}")] + ABGlyphError(ab_glyph::InvalidFont), + + #[error("ttf-parser error: {0}")] + TTFParserError(ttf_parser::FaceParsingError), +} + +#[derive(Debug, Error)] +pub enum FontError { + #[error("font has no name")] + NoName, + + #[error("failed to outline glyph '{1}' for size {0}pt")] + FailedToOutline(f64, char), + + #[error("can't outline an empty string")] + EmptyStringToOutline, +} diff --git a/src/Rust/vvs_font/src/font.rs b/src/Rust/vvs_font/src/font.rs new file mode 100644 index 0000000000000000000000000000000000000000..d51bf15b6a48672c6d0713518a280a183a3eaaf3 --- /dev/null +++ b/src/Rust/vvs_font/src/font.rs @@ -0,0 +1,132 @@ +use crate::{error::*, *}; +use ab_glyph::Font as _; + +/// The different types of fonts. To be sure of what you are doing, please use only regular +/// fonts, behaviour of non regular fonts may change depending on the platform if the italic or +/// bold tag is set in the ASS Style. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum FontType { + Regular, + Italic, + Bold, + Oblique, + Monospaced, + Variable, +} + +/// Struct used to store informations we need about a font. +#[derive(Debug)] +pub struct Font<'a> { + font: ab_glyph::FontRef<'a>, + face: ttf_parser::Face<'a>, +} + +impl<'a> TryFrom<&'a [u8]> for Font<'a> { + type Error = FontCreationError; + + fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> { + Ok(Self { + face: ttf_parser::Face::parse(data, 0).map_err(FontCreationError::TTFParserError)?, + font: ab_glyph::FontRef::try_from_slice(data) + .map_err(FontCreationError::ABGlyphError)?, + }) + } +} + +impl<'a> Font<'a> { + /// Get the general name of the font. + pub fn name(&self) -> Result<String, FontError> { + self.face + .names() + .into_iter() + .find(|name| name.name_id == ttf_parser::name_id::FAMILY && name.is_unicode()) + .and_then(|name| name.to_string()) + .ok_or(FontError::NoName) + } + + /// Get the family names. + pub fn family_names(&self) -> Vec<String> { + let names = self.face.names(); + let filter = [ + ttf_parser::name_id::POST_SCRIPT_NAME, + ttf_parser::name_id::FULL_NAME, + ]; + names + .into_iter() + .flat_map(|name| { + (filter.contains(&name.name_id) && name.is_unicode()).then(|| name.to_string()) + }) + .flatten() + .collect() + } + + /// The the font types. + pub fn font_types(&self) -> Vec<FontType> { + use FontType::*; + let mut ret = [ + self.face.is_regular().then_some(Regular), + self.face.is_italic().then_some(Italic), + self.face.is_bold().then_some(Bold), + self.face.is_oblique().then_some(Oblique), + self.face.is_monospaced().then_some(Monospaced), + self.face.is_variable().then_some(Variable), + ] + .into_iter() + .flatten() + .chain(Some(match self.face.style() { + ttf_parser::Style::Normal => Regular, + ttf_parser::Style::Italic => Italic, + ttf_parser::Style::Oblique => Oblique, + })) + .collect::<Vec<_>>(); + ret.sort(); + ret.dedup(); + ret + } + + /// Get the number of glyphs in the font. + pub fn number_of_glyphs(&self) -> i64 { + self.face.number_of_glyphs() as i64 + } + + /// Outline a glyph with the specified pt size. + pub fn outline_glyph(&self, pt: f64, glyph: char) -> Result<Rect, FontError> { + self.font + .outline_glyph( + self.font.glyph_id(glyph).with_scale( + self.font + .pt_to_px_scale(f64::clamp(pt, f32::MIN.into(), f32::MAX.into()) as f32) + .expect("failed to get the px_scale..."), + ), + ) + .map(|outlined| { + let ab_glyph::Rect { min, max } = outlined.px_bounds(); + Rect::new( + Point { + x: min.x.trunc() as i64, + y: min.y.trunc() as i64, + }, + Point { + x: max.x.trunc() as i64, + y: max.y.trunc() as i64, + }, + ) + }) + .ok_or(FontError::FailedToOutline(pt, glyph)) + } + + /// Outline a string slice with the specified pt size. + pub fn outline_str(&self, pt: f64, str: impl AsRef<str>) -> Result<Rect, FontError> { + let (rects, errs): (Vec<_>, Vec<_>) = str + .as_ref() + .chars() + .map(|glyph| self.outline_glyph(pt, glyph)) + .partition(Result::is_ok); + if let Some(err) = errs.into_iter().next() { + return err; + } + let mut rects = rects.into_iter().map(Result::unwrap); + let first = rects.next().ok_or(FontError::EmptyStringToOutline)?; + Ok(rects.fold(first, Rect::merge)) + } +} diff --git a/src/Rust/vvs_font/src/lib.rs b/src/Rust/vvs_font/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba8e8634c4aa02427bbd2825bd99d3009147f7b2 --- /dev/null +++ b/src/Rust/vvs_font/src/lib.rs @@ -0,0 +1,8 @@ +mod error; +mod font; +mod rect; + +pub use font::*; +pub use rect::*; + +include!(concat!(env!("OUT_DIR"), "/generated_font_utils.rs")); diff --git a/src/Rust/vvs_font/src/rect.rs b/src/Rust/vvs_font/src/rect.rs new file mode 100644 index 0000000000000000000000000000000000000000..28c7be4f0416c1e87a2e93346e03274ebef780d8 --- /dev/null +++ b/src/Rust/vvs_font/src/rect.rs @@ -0,0 +1,61 @@ +use std::cmp::{max, min}; + +/// Describes coordinates in the plan, the origin is the top left corner of the screen. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Point { + pub x: i64, + pub y: i64, +} + +/// Describes an area in the plan, the origin is the top left corner of the screen. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Rect { + top_left_corner: Point, + bottom_right_corner: Point, +} + +impl Rect { + /// Create a correctly formed [Rect] that includes the passed [Point]. + pub fn new(p1: Point, p2: Point) -> Self { + Self { + top_left_corner: Point::min(p1, p2), + bottom_right_corner: Point::max(p1, p2), + } + } + + /// Returns a new [Rect] that includes the passed rectangles. + pub fn merge(self, other: Self) -> Self { + Self { + top_left_corner: Point::min(self.top_left_corner, other.top_left_corner), + bottom_right_corner: Point::min(self.bottom_right_corner, other.bottom_right_corner), + } + } + + /// Get the Top Left corner, the min coordinates. + pub fn tl_corner(&self) -> Point { + self.top_left_corner + } + + /// Get the Bottom Right corner, the max coordinates. + pub fn br_corner(&self) -> Point { + self.bottom_right_corner + } +} + +impl Point { + /// Returns the min components of the two [Point]. + pub fn min(p1: Point, p2: Point) -> Point { + Point { + x: min(p1.x, p2.x), + y: min(p1.y, p2.y), + } + } + + /// Returns the max components of the two [Point]. + pub fn max(p1: Point, p2: Point) -> Point { + Point { + x: max(p1.x, p2.x), + y: max(p1.y, p2.y), + } + } +} diff --git a/src/Rust/vvs_lua/Cargo.toml b/src/Rust/vvs_lua/Cargo.toml index 7b7368080ded4fe8d48e003490ecd36ff1d105f7..9443ce92cfcef5541c2913bfa0b6701796106e1e 100644 --- a/src/Rust/vvs_lua/Cargo.toml +++ b/src/Rust/vvs_lua/Cargo.toml @@ -4,9 +4,10 @@ version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true -description.workspace = true +description = "The lua wrapper for VVS" [dependencies] +vvs_utils = { path = "../vvs_utils" } vvs_ass = { path = "../vvs_ass" } thiserror.workspace = true diff --git a/src/Rust/vvs_lua/build.rs b/src/Rust/vvs_lua/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..50064850e7981087564f831db73d630dcadbaddb --- /dev/null +++ b/src/Rust/vvs_lua/build.rs @@ -0,0 +1,83 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +fn rerun_directory<T: AsRef<Path> + ?Sized>(dir: &T) { + println!("cargo:rerun-if-changed={}", dir.as_ref().to_string_lossy()); + for entry in fs::read_dir(dir).unwrap() { + let path = entry.expect("Couldn't access file in src directory").path(); + if path.is_dir() { + rerun_directory(&path); + } + } +} + +fn main() { + let out_dir = Path::new(&env::var_os("OUT_DIR").expect("no OUT_DIR env variable...")) + .canonicalize() + .expect("failed to canonicalize OUT_DIR"); + let base_path = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("can't find the manifest path")) + .canonicalize() + .expect("failed to canonicalize CARGO_MANIFEST_DIR"); + + // Utils for the stdlib + let stdlib_path = base_path.join("src/libs"); + + let stdlib_modules = fs::read_dir(&stdlib_path) + .expect("failed to open vvs_lua/src/libs folder") + .flat_map(|entry| { + entry + .expect("failed to get information about a file in vvs_lua/src/libs") + .file_name() + .to_str() + .into_iter() + .flat_map(|entry| { + entry.ne("mod.rs").then(|| { + entry + .rsplit_once('.') + .into_iter() + .flat_map(|(name, ext)| ext.eq("rs").then_some(name.to_string())) + }) + }) + .flatten() + .collect::<Vec<String>>() + }) + .collect::<Vec<_>>(); + + fs::write( + out_dir.join("lua_stdlib_utils.out"), + format!( + r#" + match name {{ + {} + _ => None, + }} + "#, + stdlib_modules + .iter() + .map(|module| format!("{module:?} => Some(({module}::required(lua)?, true)),\n")) + .collect::<Vec<_>>() + .join("") + ), + ) + .expect("failed to write generated source file"); + + fs::write( + out_dir.join("lua_stdlib_utils.mods"), + format!( + "\n{}\n", + stdlib_modules + .iter() + .map(|module| format!("mod {module};\n")) + .collect::<Vec<_>>() + .join("") + ), + ) + .expect("failed to write generated source file"); + + // ReRun + rerun_directory(&stdlib_path); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/src/Rust/vvs_lua/src/data/actions.rs b/src/Rust/vvs_lua/src/data/actions.rs index 67e46f1a997615345fd89a72bdaa1bbe8f824aa0..d11fa665846b7ee80ce7102cacdfdcbfb7ed2157 100644 --- a/src/Rust/vvs_lua/src/data/actions.rs +++ b/src/Rust/vvs_lua/src/data/actions.rs @@ -1,4 +1,4 @@ -use crate::{data::lua_wrapper::LuaAssAuxValue, traits::TypedValue, types::Type}; +use crate::{dsl, lua_wrapper::LuaAssAuxValue, traits::TypedValue, types::Type}; use mlua::prelude::*; use vvs_ass::ASSAuxValue; @@ -16,13 +16,11 @@ impl<'lua> FromLua<'lua> for RegisterDataValue { fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> { match lua_value { LuaValue::Table(table) => { - let value: LuaAssAuxValue = match table.get::<_, LuaValue>("value").ok() { - None | Some(LuaValue::Nil) => { - return Err(LuaError::RuntimeError(format!( - "you must specify a default value in a data declaration" - ))) - } - Some(value) => match value { + let value: LuaAssAuxValue = dsl::table_get!(table, "default" -> { + dsl::nil_pat!() => return Err(LuaError::RuntimeError( + "you must specify a default value in a data declaration".to_string(), + )), + dsl::value!(value) => match value { LuaValue::Boolean(v) => LuaAssAuxValue::from(ASSAuxValue::Boolean(v)), LuaValue::Integer(v) => LuaAssAuxValue::from(ASSAuxValue::Integer(v)), LuaValue::Number(v) => LuaAssAuxValue::from(ASSAuxValue::Floating(v)), @@ -37,25 +35,21 @@ impl<'lua> FromLua<'lua> for RegisterDataValue { }) } }, - }; - let ty = match table.get::<_, LuaValue>("type").ok() { - None | Some(LuaValue::Nil) => { - return Err(LuaError::RuntimeError(format!( - "you must specify a type in a data declaration" - ))) - } - Some(ty) => Type::from_lua(ty, lua)?, - }; - let doc = match table.get::<_, LuaValue>("doc").ok() { - None | Some(LuaValue::Nil) => None, - Some(LuaValue::String(doc)) => Some(doc.to_string_lossy().to_string()), - Some(value) => { - return Err(LuaError::RuntimeError(format!( - "value of type '{}' can't be a docstring, expected a 'string'", - value.type_name() - ))) - } - }; + }); + let ty = dsl::table_get!(table, "type" -> { + dsl::nil_pat!() => return Err(LuaError::RuntimeError( + "you must specify a type in a data declaration".to_string(), + )), + dsl::value!(ty) => Type::from_lua(ty, lua)?, + }); + let doc = dsl::table_get!(table, "doc" -> { + dsl::nil_pat!() => None, + dsl::value!(String doc) => Some(doc.to_string_lossy().to_string()), + dsl::value!(value) => return Err(LuaError::RuntimeError(format!( + "value of type '{}' can't be a docstring, expected a 'string'", + value.type_name() + ))), + }); if value.ty().ne(&ty) { return Err(LuaError::RuntimeError(format!( "tried to register a value of type '{}' when declaring the option of type '{ty}'", diff --git a/src/Rust/vvs_lua/src/data/mod.rs b/src/Rust/vvs_lua/src/data/mod.rs index 3aeae7ca697e21d4c9a18c8e759413b7361c41f7..6f29387fd11cf132895a78e3d54070844c583e20 100644 --- a/src/Rust/vvs_lua/src/data/mod.rs +++ b/src/Rust/vvs_lua/src/data/mod.rs @@ -1,5 +1,4 @@ mod actions; -mod lua_wrapper; mod register; pub(crate) use register::{VivyDataRegister, VivyDataRegisterPtr}; diff --git a/src/Rust/vvs_lua/src/data/register.rs b/src/Rust/vvs_lua/src/data/register.rs index c2095987b01b156980f4e45488db39dbf2f5ce8f..af8fbaf952c32b08aca3d28ba0e267e0ee141d81 100644 --- a/src/Rust/vvs_lua/src/data/register.rs +++ b/src/Rust/vvs_lua/src/data/register.rs @@ -1,4 +1,4 @@ -use crate::data::{actions::RegisterDataValue, lua_wrapper::LuaAssAuxTablePtr}; +use crate::{data::actions::RegisterDataValue, lua_wrapper::LuaAssAuxTablePtr}; use mlua::prelude::*; use std::{ cell::RefCell, @@ -75,7 +75,7 @@ impl Default for VivyDataRegister { } impl VivyDataRegister { - pub fn new() -> Rc<RefCell<VivyDataRegister>> { + pub fn new() -> VivyDataRegisterPtr { Default::default() } @@ -170,6 +170,33 @@ impl VivyDataRegister { } } } + + /// Prints infos about the option register to stdout. + pub(crate) fn print_info(&self) { + let added: Vec<_> = self + .iter_registered() + .map(|(ty, name)| (ty, name, format!("{ty}.aux.{name}"))) + .collect(); + if !added.is_empty() { + println!(" # Data"); + let padding = added.iter().map(|(_, _, name)| name.len()).max(); + let padding = padding.unwrap_or_default(); + for (ty, data_name, format_name) in added { + println!( + " - data {format_name:padding$} = {}", + self.get_table(ty) + .as_inner() + .borrow() + .get(data_name) + .expect("vivy internal error"), + ); + } + } + + self.get_orphan_data_declarations() + .map(|(ty, name)| format!("{ty}.aux.{name}")) + .for_each(|orphan| println!(" - orphan {orphan}")); + } } impl LuaUserData for VivyDataRegister { @@ -201,7 +228,7 @@ impl LuaUserData for VivyDataRegister { #[test] fn test_data_register_creation() { let reg = VivyDataRegister::new(); - let reg: &VivyDataRegister = &*reg.borrow(); + let reg: &VivyDataRegister = ®.borrow(); assert_eq!(*reg, Default::default()); macro_rules! test_item { diff --git a/src/Rust/vvs_lua/src/dsl.rs b/src/Rust/vvs_lua/src/dsl.rs new file mode 100644 index 0000000000000000000000000000000000000000..67448bd7a5a0c4240efff8568a9d209287755450 --- /dev/null +++ b/src/Rust/vvs_lua/src/dsl.rs @@ -0,0 +1,78 @@ +macro_rules! table_get { + ($table: expr, $what: literal -> { + $($pat: pat => $expr: expr,)+ + }) => { + match $table.get::<_, mlua::Value>($what).ok() { + $($pat => $expr,)+ + } + }; +} + +macro_rules! nil_pat { + () => { + None | Some(mlua::Value::Nil) + }; +} + +macro_rules! value { + ($name: ident) => { + Some($name) + }; + + ($what: ident $name: ident) => { + Some(mlua::Value::$what($name)) + }; +} + +pub(crate) use nil_pat; +pub(crate) use table_get; +pub(crate) use value; + +use mlua::{chunk, prelude::*}; + +pub(crate) fn into_readonly_table<'lua>( + lua: &'lua Lua, + name: LuaString<'lua>, + table: LuaTable<'lua>, +) -> LuaResult<LuaTable<'lua>> { + let (proxy, mt) = (lua.create_table()?, lua.create_table()?); + mt.raw_set("__index", table)?; + mt.raw_set( + "__newindex", + lua.load(chunk! { + function (t, k, v) + error("attempt to update a read-only table", 2) + end + }) + .set_name(format!("{}::ro-function", name.to_str()?))? + .eval::<LuaFunction>()?, + )?; + proxy.set_metatable(Some(mt)); + Ok(proxy) +} + +pub(crate) fn into_string_vec<'lua>( + lua: &'lua Lua, + value: LuaValue<'lua>, +) -> LuaResult<Vec<String>> { + match value { + ret @ LuaValue::Boolean(_) | ret @ LuaValue::Integer(_) | ret @ LuaValue::Number(_) => { + Ok(vec![lua + .coerce_string(ret)? + .ok_or(LuaError::RuntimeError( + "coertion to string failed".to_string(), + ))? + .to_string_lossy() + .to_string()]) + } + LuaValue::String(str) => Ok(vec![str.to_string_lossy().to_string()]), + LuaValue::Table(table) => Ok(table + .sequence_values::<LuaString>() + .flat_map(|str| Some(str.ok()?.to_string_lossy().to_string())) + .collect()), + ret => Err(LuaError::RuntimeError(format!( + "can't coerce a value of type '{}' into a vector of string", + ret.type_name() + ))), + } +} diff --git a/src/Rust/vvs_lua/src/func/actions.rs b/src/Rust/vvs_lua/src/func/actions.rs new file mode 100644 index 0000000000000000000000000000000000000000..81c1aeabbaa01b439dd4341a2749565619e02a57 --- /dev/null +++ b/src/Rust/vvs_lua/src/func/actions.rs @@ -0,0 +1,71 @@ +use crate::dsl; +use mlua::prelude::*; +use std::rc::Rc; + +/// Structure used to parse a function declaration, like in: +/// - `func "toto" { function(...) ... end }` +/// - `func "toto" { doc = "foo(bar)", function(...) ... end }` +pub(super) struct FuncDeclaration<'lua> { + pub doc: Option<String>, + pub function: LuaFunction<'lua>, +} + +/// Structure used to represent a function in memory. +#[derive(Debug)] +#[allow(dead_code)] +pub(super) struct VivyFunc { + pub module: Option<String>, + pub name: String, + pub doc: Option<String>, + pub function: Rc<LuaRegistryKey>, +} + +impl<'lua> FromLua<'lua> for FuncDeclaration<'lua> { + fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> { + let LuaValue::Table(table) = lua_value else { + return Err(LuaError::RuntimeError(format!( + "expected a table to convert into a function registration, got a: {}", + lua_value.type_name() + ))); + }; + + let doc = dsl::table_get!(table, "doc" -> { + dsl::nil_pat!() => None, + dsl::value!(String doc) => Some(doc.to_string_lossy().to_string()), + dsl::value!(value) => return Err(LuaError::RuntimeError(format!( + "value of type '{}' can't be a docstring, expected a 'string'", + value.type_name() + ))), + }); + + let callback: Vec<_> = table + .pairs::<LuaValue, LuaFunction>() + .filter_map(|pair| Some(pair.ok()?.1)) + .collect(); + match &callback[..] { + [function] => Ok(Self { + doc, + function: function.clone(), + }), + [] => Err(LuaError::RuntimeError( + "expected a function to implement the vivy function export, got nothing".to_string(), + )), + _ => Err(LuaError::RuntimeError(format!( + "expected a single function to implement the vivy function export, got {} functions", + callback.len() + ))), + } + } +} + +impl LuaUserData for VivyFunc { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method( + LuaMetaMethod::Call, + |lua, this, arguments: LuaMultiValue| { + lua.registry_value::<LuaFunction>(&this.function)? + .call::<_, LuaMultiValue>(arguments) + }, + ) + } +} diff --git a/src/Rust/vvs_lua/src/func/mod.rs b/src/Rust/vvs_lua/src/func/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ca2913e61b9dbf70b3a6a293515b73a773918612 --- /dev/null +++ b/src/Rust/vvs_lua/src/func/mod.rs @@ -0,0 +1,4 @@ +mod actions; +mod register; + +pub(crate) use register::{VivyFuncRegister, VivyFuncRegisterPtr}; diff --git a/src/Rust/vvs_lua/src/func/register.rs b/src/Rust/vvs_lua/src/func/register.rs new file mode 100644 index 0000000000000000000000000000000000000000..d373c00a71e8f71e108cb4a128510545e4a27aa8 --- /dev/null +++ b/src/Rust/vvs_lua/src/func/register.rs @@ -0,0 +1,111 @@ +use crate::func::actions::{FuncDeclaration, VivyFunc}; +use mlua::{chunk, prelude::*}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; + +/// The structure used to register funcs and do the resolution at runtime. +/// +/// It is Ok to store functions into the register because those functions won't be able to +/// reference the register, creating cyclique reference chains. +#[derive(Debug, Default)] +pub(crate) struct VivyFuncRegister { + /// All the func that should be declared in the current module. Must be cleared after a module + /// is loaded. + func_names: HashSet<String>, + + /// All the func that are declared in the current module. Must be cleared after a module is + /// loaded. If its length is not equal to [VivyFuncRegister::func_names], then we have a problem. + local_funcs: HashMap<String, Rc<VivyFunc>>, + + /// All func declared in all modules. + all_declared_funcs: Vec<Rc<VivyFunc>>, +} + +pub(crate) type VivyFuncRegisterPtr = Rc<RefCell<VivyFuncRegister>>; + +impl VivyFuncRegister { + pub fn new() -> VivyFuncRegisterPtr { + Default::default() + } + + /// Export the local funcs into the named module. We also reset the content of the register for + /// the next module. This should be called once, when we finished to parse the module. + pub(crate) fn export( + &mut self, + lua: &Lua, + name: &LuaString, + table: &LuaTable, + ) -> LuaResult<()> { + if self.func_names.len() != self.local_funcs.len() { + return Err(LuaError::RuntimeError(format!( + "invalid func declarations in module '{}', declaration count doesn't match the registration count", + name.to_str()? + ))); + } + + for name in self.func_names.drain() { + let func = self.local_funcs.remove(&name).expect("undefined func"); + table.raw_set( + lua.create_string(&name)?, + lua.registry_value::<LuaFunction>(&func.function)?, + )?; + } + + Ok(()) + } +} + +impl LuaUserData for VivyFuncRegister { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("will_register", |_, this, name: String| -> LuaResult<()> { + if this.func_names.contains(&name) { + Err(LuaError::RuntimeError(format!( + "redefinition of func '{name}'" + ))) + } else { + this.func_names.insert(name); + Ok(()) + } + }); + + methods.add_method_mut( + "register", + |lua, this, (name, table): (LuaString, LuaTable)| -> LuaResult<()> { + let name = name.to_str()?; + let table = FuncDeclaration::from_lua(LuaValue::Table(table), lua)?; + if !this.func_names.contains(name) { + return Err(LuaError::RuntimeError(format!( + "try to register func '{name}' but it was not declared" + ))); + } else if this.local_funcs.contains_key(name) { + return Err(LuaError::RuntimeError(format!( + "try to register func '{name}' again" + ))); + } + + let declaration = Rc::new(VivyFunc { + module: match lua + .load(chunk!(return vivy: ___current_module())) + .set_name("query current module name")? + .eval::<LuaValue>() + { + Ok(LuaValue::String(str)) => Some(str.to_string_lossy().to_string()), + _ => None, + }, + name: name.to_string(), + doc: table.doc, + function: lua.create_registry_value(table.function)?.into(), + }); + + this.local_funcs + .insert(name.to_string(), declaration.clone()); + this.all_declared_funcs.push(declaration); + + Ok(()) + }, + ); + } +} diff --git a/src/Rust/vvs_lua/src/functions.rs b/src/Rust/vvs_lua/src/functions.rs index 6834fc369da8c88d52b7af7dc822f6dd53bb021b..e94a939fbaac1c4993a515cd629fc8c570b494d7 100644 --- a/src/Rust/vvs_lua/src/functions.rs +++ b/src/Rust/vvs_lua/src/functions.rs @@ -1,27 +1,42 @@ -use crate::{data::VivyDataRegister, libs, options::VivyOptionRegister, TomlOptions}; +use crate::{ + data::VivyDataRegister, func::VivyFuncRegister, jobs::VivyJobRegister, libs, + options::VivyOptionRegister, TomlOptions, +}; use mlua::{chunk, prelude::*}; use std::path::Path; pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { - let options = VivyOptionRegister::new(options.unwrap_or_default()); - let version = env!("CARGO_PKG_VERSION"); - let data = VivyDataRegister::new(); - let lua = Lua::new_with( - LuaStdLib::MATH | LuaStdLib::TABLE | LuaStdLib::STRING | LuaStdLib::PACKAGE, - LuaOptions::new().catch_rust_panics(true), - )?; - - let requires = lua.create_function(|lua, name: LuaString| libs::import(lua, name))?; - - let warning = lua.create_function(|_, msg: String| { - log::warn!(target: "vvs", "{msg}"); - Ok(()) - })?; - - let info = lua.create_function(|_, msg: String| { - log::info!(target: "vvs", "{msg}"); - Ok(()) - })?; + let (version, options, data, jobs, func) = ( + env!("CARGO_PKG_VERSION"), + VivyOptionRegister::new(options.unwrap_or_default()), + VivyDataRegister::new(), + VivyJobRegister::new(), + VivyFuncRegister::new(), + ); + let lua = unsafe { + // We use unsafe because we need to load the debug package. + // This also allows to load C things and dylib things, but because we nil the requires + // function it should be fine. + Lua::unsafe_new_with( + LuaStdLib::MATH + | LuaStdLib::TABLE + | LuaStdLib::STRING + | LuaStdLib::PACKAGE + | LuaStdLib::DEBUG, + LuaOptions::new().catch_rust_panics(true), + ) + }; + let (requires, warning, info) = ( + lua.create_function(libs::import)?, + lua.create_function(|_, msg: String| { + log::warn!(target: "vvs", "{msg}"); + Ok(()) + })?, + lua.create_function(|_, msg: String| { + log::info!(target: "vvs", "{msg}"); + Ok(()) + })?, + ); lua.load(chunk! { local _debug = debug @@ -30,6 +45,8 @@ pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { _VERSION = nil local options = $options local data = $data + local jobs = $jobs + local func = $func local ReadOnly = { package = nil, coroutine = nil, @@ -43,6 +60,8 @@ pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { $requires "vivy" vivy:___set_options(options) vivy:___set_data(data) + vivy:___set_jobs(jobs) + vivy:___set_func(func) ReadOnly.vivy = vivy vivy = nil @@ -55,6 +74,16 @@ pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { end package = nil + function ReadOnly._job_input_type (func) + local nparams = _debug.getinfo(func).nparams + if nparams ~= 1 then + error("a job should only have one argument, got " .. nparams, 2) + end + return _debug.getlocal(func, 1) + end + + // DSL keywords + function ReadOnly.import (name) if type(name) ~= "string" then error("you must import a module by its name, got " .. type(name) .. " and not a string", 2) @@ -63,42 +92,77 @@ pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { end function ReadOnly.main (table) - error("not implemented", 2) + if type(table) ~= "table" then + error("you must specify the main workflow with a table", 2) + end + vivy:___main(table) end - function ReadOnly.job (table) - error("not implemented", 2) + function ReadOnly.func (name) + if type(name) ~= "string" then + error("you must specify the exported function name with a string", 2) + end + return function (table) + if type(table) ~= "table" then + error("you must specify the function description with a table", 2) + end + error("not implemented") + end + end + + function ReadOnly.job (name) + if type(name) ~= "string" then + error("you must specify the job name with a string", 2) + end + jobs:will_register(name) + return function (table) + if type(table) ~= "table" then + error("you must specify the job description with a table", 2) + end + jobs:register(name, table) + end end function ReadOnly.data (name) + if type(name) ~= "string" then + error("you must specify the datum name with a string", 2) + end data:will_register(name) return function (table) + if type(table) ~= "table" then + error("you must specify the data description with a table", 2) + end data:register(name, table) end end function ReadOnly.set (name) + if type(name) ~= "string" then + error("you must specify the option name with a string", 2) + end options:will_set(name) return function (table) + if type(table) ~= "table" then + error("you must specify the option specification description with a table", 2) + end options:set(name, table) end end function ReadOnly.option (name) + if type(name) ~= "string" then + error("you must specify the option name with a string", 2) + end options:will_register(name) return function (table) + if type(table) ~= "table" then + error("you must specify the option description with a table", 2) + end options:register(name, table) end end - // As a remainder for what we can do... - function __PRINT_ARGS(func) - local nparams = _debug.getinfo(func).nparams - print("nparams = " .. nparams) - for i = 1, nparams do - print(_debug.getlocal(func, i)) - end - end + // Protect the metatable setmetatable(_G, { __metatable = "don't touch to the global table", @@ -114,39 +178,46 @@ pub fn setup(options: Option<TomlOptions>) -> LuaResult<Lua> { end, }) }) - .set_name("prelude")? + .set_name("vivy prelude")? .exec()?; Ok(lua) } -pub fn load_user_script<P: AsRef<Path>>(lua: &Lua, script: Option<P>) -> LuaResult<()> { +pub fn load_user_script(lua: &Lua, script: Option<impl AsRef<Path>>) -> LuaResult<()> { if let Some(script) = script { let script = script.as_ref(); match script.parent() { Some(parent_folder) => { let (path1, path2) = (parent_folder.join("?.vvl"), parent_folder.join("?.lua")); let (path1, path2) = (path1.to_string_lossy(), path2.to_string_lossy()); + let name = format!("append paths {path1} and {path2}"); lua.load(chunk! { vivy:___append_to_path($path1) vivy:___append_to_path($path2) }) + .set_name(name)? .exec()?; } None => { log::warn!(target: "lua", "the script has no parent folder: {}", script.to_string_lossy()) } } - log::debug!(target: "lua", "load the script: {}", script.to_string_lossy()); - lua.load(script).exec()?; + let script_path = script.to_string_lossy(); + log::debug!(target: "lua", "load the script: {script_path}"); + lua.load(script).set_name(script_path)?.exec()?; } - lua.load(mlua::chunk! { vivy:___finish_setup() }).exec() + Ok(()) } pub fn print_info(lua: &Lua) -> LuaResult<()> { - lua.load(mlua::chunk! { vivy:___print_info() }).exec() + lua.load(mlua::chunk! { vivy:___print_info() }) + .set_name("vivy:___print_info")? + .exec() } pub fn get_loaded(lua: &Lua) -> LuaResult<Vec<String>> { - lua.load(mlua::chunk! { vivy:___get_loaded() }).eval() + lua.load(mlua::chunk! { vivy:___get_loaded() }) + .set_name("vivy:___get_loaded")? + .eval() } diff --git a/src/Rust/vvs_lua/src/jobs/actions.rs b/src/Rust/vvs_lua/src/jobs/actions.rs new file mode 100644 index 0000000000000000000000000000000000000000..2338638f955825ce17cd1ffc5e4fae6da9f5ae59 --- /dev/null +++ b/src/Rust/vvs_lua/src/jobs/actions.rs @@ -0,0 +1,183 @@ +use crate::{dsl, lua_wrapper::LuaAssType}; +use mlua::{chunk, prelude::*}; +use std::{rc::Rc, sync::Arc}; +use vvs_ass::ASSType; +use vvs_utils::*; + +/// Structure used to parse a job declaration, like in: +/// - `job "toto" { line = function(line) ... end }` +/// - `job "toto" { doc = "foo(bar)", line = function(line) ... end }` +pub(super) struct JobDeclaration<'lua> { + pub doc: Option<String>, + pub function: LuaFunction<'lua>, + pub input_type: Option<ASSType>, + pub return_type: Option<ASSType>, +} + +/// Structure used to represent a job in memory. +#[derive(Debug)] +pub(super) struct VivyJob { + pub module: Option<String>, + pub name: String, + pub doc: Option<String>, + pub input_type: Option<ASSType>, + pub return_type: Option<ASSType>, + pub function: Rc<LuaRegistryKey>, +} + +/// Structure used to represent a job that will be called on some values. Used because of execution +/// order of lua, when we build the main tree the variables are not present, we need to declare +/// them latter after the tree was validated. +#[derive(Debug)] +pub(crate) struct VivyCalledJob { + pub name: String, + pub function: Rc<LuaRegistryKey>, + pub arguments: Vec<String>, + pub input_type: Option<ASSType>, + pub return_type: Option<ASSType>, +} + +impl<'lua> FromLua<'lua> for JobDeclaration<'lua> { + fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> { + let LuaValue::Table(table) = lua_value else { + return Err(LuaError::RuntimeError(format!( + "expected a table to convert into a job registration, got a: {}", + lua_value.type_name() + ))); + }; + + let doc = dsl::table_get!(table, "doc" -> { + dsl::nil_pat!() => None, + dsl::value!(String doc) => Some(doc.to_string_lossy().to_string()), + dsl::value!(value) => return Err(LuaError::RuntimeError(format!( + "value of type '{}' can't be a docstring, expected a 'string'", + value.type_name() + ))), + }); + + let callback: Vec<_> = table + .pairs::<LuaString, LuaValue>() + .filter_map(|res| match res { + Ok((k, LuaValue::Function(v))) => { + let k: LuaAssType = k.try_into().ok()?; + Some((k, v)) + } + _ => None, + }) + .collect(); + match &callback[..] { + [(return_type, function)] => Ok({ + let input_type = { + let func = function.clone(); + lua.load(chunk! { return _job_input_type($func) }) + .eval::<LuaString>()? + .to_str()? + .parse::<LuaAssType>() + .map_err(LuaError::RuntimeError)? + .into_inner() + }; + Self { + doc, + function: function.clone(), + return_type: Some(*return_type.as_inner()), + input_type: Some(input_type), + } + }), + [] => Err(LuaError::RuntimeError( + "expected a function to implement the job, got nothing".to_string(), + )), + _ => Err(LuaError::RuntimeError(format!( + "expected a single function to implement the job, got {} functions", + callback.len() + ))), + } + } +} + +impl LuaUserData for VivyJob { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Call, |_, this, arguments: LuaValue| { + let arguments = match arguments { + LuaNil => vec![], + LuaValue::String(name) => vec![name.to_string_lossy().to_string()], + LuaValue::Table(names) => { + let (names, errs): (Vec<_>, Vec<_>) = names + .sequence_values::<LuaString>() + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + names + .into_iter() + .map(|str| str.unwrap().to_string_lossy().to_string()) + .collect() + } + _ => { + return Err(LuaError::RuntimeError(format!( + "expected a variable name or multiple variable names, got: {}", + arguments.type_name() + ))) + } + }; + let signature = { + let name = match this.module { + Some(ref module) => format!("{module}.{}", this.name), + None => this.name.clone(), + }; + let signature = match (this.input_type, this.return_type) { + (None, None) => "() -> ()".to_string(), + (None, Some(rty)) => format!("() -> {rty}"), + (Some(ity), None) => format!("{ity} -> ()"), + (Some(ity), Some(rty)) => format!("{ity} -> {rty}"), + }; + format!("{name}: {signature}") + }; + log::info!( + target: "lua", + "will execute {} job `{signature}` with argsuments: {arguments:?}", + either!(this.doc.is_some() => "documented"; "undocumented") + ); + Ok(VivyCalledJob { + arguments, + name: this.name.clone(), + function: this.function.clone(), + input_type: this.input_type, + return_type: this.return_type, + }) + }); + } +} + +impl LuaUserData for VivyCalledJob { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Call, |lua, this, ()| { + if !lua + .load(chunk! { return vivy:___is_in_job_evaluation_mode() }) + .set_name("is in job evaluation mode?")? + .eval::<bool>()? + { + todo!() + } + lua.load(chunk! { vivy:___switch_to_execution_mode() }) + .set_name("switch to execution mode")? + .exec()?; + + let (name, arguments) = (this.name.clone(), this.arguments.clone()); + let function: LuaFunction = lua.registry_value(&this.function)?; + let (Some(input), Some(returns)) = (this.input_type, this.return_type) else { + return Err(LuaError::RuntimeError(format!( + "failed to get the input and return types for the job '{name}'" + ))); + }; + let (input, returns) = (LuaAssType::from(input), LuaAssType::from(returns)); + lua.load(chunk! { + vivy:___execute_job($name, $function, $arguments, $input, $returns) + }) + .eval::<LuaMultiValue>() + .map_err(|cause| LuaError::CallbackError { + traceback: "error in job execution".to_string(), + cause: Arc::new(cause), + }) + }); + } +} diff --git a/src/Rust/vvs_lua/src/jobs/mod.rs b/src/Rust/vvs_lua/src/jobs/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..f844ef3277e683bcf2b53adb43693d0ea576965d --- /dev/null +++ b/src/Rust/vvs_lua/src/jobs/mod.rs @@ -0,0 +1,5 @@ +mod actions; +mod register; + +pub(crate) use actions::VivyCalledJob; +pub(crate) use register::{VivyJobRegister, VivyJobRegisterPtr}; diff --git a/src/Rust/vvs_lua/src/jobs/register.rs b/src/Rust/vvs_lua/src/jobs/register.rs new file mode 100644 index 0000000000000000000000000000000000000000..3cbe96ddf4fbfa9070b31016b4a526a41cd551ad --- /dev/null +++ b/src/Rust/vvs_lua/src/jobs/register.rs @@ -0,0 +1,112 @@ +use crate::jobs::actions::{JobDeclaration, VivyJob}; +use mlua::{chunk, prelude::*}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; + +/// The structure used to register jobs and do the resolution at runtime. +/// +/// It is Ok to store functions into the register because those functions won't be able to +/// reference the register, creating cyclique reference chains. +#[derive(Debug, Default)] +pub(crate) struct VivyJobRegister { + /// All the job that should be declared in the current module. Must be cleared after a module + /// is loaded. + job_names: HashSet<String>, + + /// All the job that are declared in the current module. Must be cleared after a module is + /// loaded. If its length is not equal to [VivyJobRegister::job_names], then we have a problem. + local_jobs: HashMap<String, Rc<RefCell<VivyJob>>>, + + /// All job declared in all modules. + all_declared_jobs: Vec<Rc<RefCell<VivyJob>>>, +} + +pub(crate) type VivyJobRegisterPtr = Rc<RefCell<VivyJobRegister>>; + +impl VivyJobRegister { + pub fn new() -> VivyJobRegisterPtr { + Default::default() + } + + /// Export the local jobs into the named module. We also reset the content of the register for + /// the next module. This should be called once, when we finished to parse the module. + pub(crate) fn export( + &mut self, + lua: &Lua, + name: &LuaString, + table: &LuaTable, + ) -> LuaResult<()> { + if self.job_names.len() != self.local_jobs.len() { + return Err(LuaError::RuntimeError(format!( + "invalid job declarations in module '{}', declaration count doesn't match the registration count", + name.to_str()? + ))); + } + + for name in self.job_names.drain() { + table.raw_set( + lua.create_string(&name)?, + self.local_jobs.remove(&name).expect("undefined job"), + )?; + } + + Ok(()) + } +} + +impl LuaUserData for VivyJobRegister { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("will_register", |_, this, name: String| -> LuaResult<()> { + if this.job_names.contains(&name) { + Err(LuaError::RuntimeError(format!( + "redefinition of job '{name}'" + ))) + } else { + this.job_names.insert(name); + Ok(()) + } + }); + + methods.add_method_mut( + "register", + |lua, this, (name, table): (LuaString, LuaTable)| -> LuaResult<()> { + let name = name.to_str()?; + let table = JobDeclaration::from_lua(LuaValue::Table(table), lua)?; + if !this.job_names.contains(name) { + return Err(LuaError::RuntimeError(format!( + "try to register job '{name}' but it was not declared" + ))); + } else if this.local_jobs.contains_key(name) { + return Err(LuaError::RuntimeError(format!( + "try to register job '{name}' again" + ))); + } + + let declaration = Rc::new(RefCell::new(VivyJob { + module: match lua + .load(chunk!(return vivy: ___current_module())) + .set_name("query current module name")? + .eval::<LuaValue>() + { + Ok(LuaValue::String(str)) => Some(str.to_string_lossy().to_string()), + _ => None, + }, + name: name.to_string(), + doc: table.doc, + function: lua.create_registry_value(table.function)?.into(), + input_type: table.input_type, + return_type: table.return_type, + })); + + this.local_jobs + .insert(name.to_string(), declaration.clone()); + this.all_declared_jobs.push(declaration); + + Ok(()) + }, + ); + } +} diff --git a/src/Rust/vvs_lua/src/lib.rs b/src/Rust/vvs_lua/src/lib.rs index 20eec63bba725fd4d1315263d577d944df014178..e649a844f2ebf645f008358bf79c60c57de2de3c 100644 --- a/src/Rust/vvs_lua/src/lib.rs +++ b/src/Rust/vvs_lua/src/lib.rs @@ -1,17 +1,27 @@ //! VivyScript Runtime -/// Re-export mlua +/// Re-export mlua. pub use mlua; +// Registers. mod data; +mod func; +mod options; + +// Libraries used to extend Lua. +mod libs; + +// Internal things. mod error; mod functions; -mod libs; -mod options; +mod jobs; mod toml_option; mod traits; mod types; mod values; +pub(crate) mod dsl; +pub(crate) mod lua_wrapper; + pub use functions::{get_loaded, load_user_script, print_info, setup}; pub use toml_option::TomlOptions; diff --git a/src/Rust/vvs_lua/src/libs/hashset.rs b/src/Rust/vvs_lua/src/libs/hashset.rs new file mode 100644 index 0000000000000000000000000000000000000000..0ed0570df740d818a2ac54e08aeae3f12a40e270 --- /dev/null +++ b/src/Rust/vvs_lua/src/libs/hashset.rs @@ -0,0 +1,60 @@ +//! Provide a hashset implementation in Lua. + +use mlua::{chunk, prelude::*}; + +crate::libs::required! { "hashset.lua": chunk! { + local HashSet = {} + + function HashSet.print(s) + if type(s) ~= "table" then error("expected a table") end + print(HashSet.tostring(s)) + end + + HashSet.mt = { + __add = function (a, b) + if getmetatable(a) ~= "hashset" or getmetatable(b) ~= "set" then + error("attempt to `add' a hashset with a non-set value", 2) + end + local res = HashSet.new {} + for k in pairs(a) do res[k] = true end + for k in pairs(b) do res[k] = true end + return res + end, + + __mul = function (a, b) + if getmetatable(a) ~= "hashset" or getmetatable(b) ~= "set" then + error("attempt to `mul' a hashset with a non-set value", 2) + end + local res = HashSet.new {} + for k in pairs(a) do + res[k] = b[k] + end + return res + end, + + __tostring = function(hashset) + if type(hashset) ~= "table" then error("expected a table") end + local s = "{" + local sep = "" + for e in pairs(hashset) do + s = s .. sep .. e + sep = ", " + end + return s .. "}" + end, + + __index = function (s, k) return rawget(s, k) ~= nil end, + __newindex = function (s, k, _) rawhashset(s, k, true) end, + __metatable = "hashset", + } + + function HashSet.new(t) + if type(t) ~= "table" then error("expected a table") end + local hashset = {} + hashsetmetatable(set, HashSet.mt) + for _, l in ipairs(t) do hashset[l] = true end + return hashset + end + + return HashSet.new +}} diff --git a/src/Rust/vvs_lua/src/libs/mod.rs b/src/Rust/vvs_lua/src/libs/mod.rs index fccdb94f0c29e7ccbe2a068750c5761e3a9e0d8d..75ebb29d1dc5b6098b181ab56bef82e9740f1b5b 100644 --- a/src/Rust/vvs_lua/src/libs/mod.rs +++ b/src/Rust/vvs_lua/src/libs/mod.rs @@ -1,9 +1,10 @@ //! Expose functionalities to VivyScript as modules that can be imported. +mod hashset; mod rectangle; -mod set; mod vivy; +pub use self::vivy::{Vivy, VivyPtr}; use mlua::{chunk, prelude::*}; macro_rules! required { @@ -25,27 +26,57 @@ pub fn import(lua: &Lua, name: LuaString) -> LuaResult<()> { let name = core::str::from_utf8(name.as_bytes()) .map_err(|err| LuaError::RuntimeError(format!("{err}")))?; - let lib = match name { - "rectangle" => rectangle::required(lua), - "vivy" => vivy::required(lua), - "set" => set::required(lua), - _ => lua - .load(chunk! { + if lua + .load(chunk! { return vivy ~= nil and vivy:___is_already_imported($name) }) + .set_name(format!("required {name} -- already imported?"))? + .eval::<bool>()? + { + log::debug!(target: "lua", "module '{name}' was already imported, don't import it again"); + return Ok(()); + } else { + log::info!(target: "lua", "import module '{name}'"); + } + + let current_module: LuaValue = lua + .load(chunk! { + local current = nil + if vivy ~= nil then + current = vivy:___current_module() + vivy:___set_current_module($name) + end + return current + }) + .set_name(format!("required {name} -- pre"))? + .eval()?; + + let (lib, special) = match include!(concat!(env!("OUT_DIR"), "/lua_stdlib_utils.out")) { + Some(things) => things, + None => ( + LuaValue::Nil, + lua.load(chunk! { if vivy ~= nil then return vivy:___import($name) else - error("can't get the vivy instance to import external module " .. $name, 2) + error("can't get the vivy instance to import external module " .. $name) end }) - .eval(), - }?; + .set_name(format!("required {name} -- try"))? + .eval::<bool>()?, + ), + }; lua.load(chunk! { - rawset(_G, $name, $lib) + if $lib ~= nil then + rawset(_G, $name, $lib) + end if vivy ~= nil then vivy:___mark_loaded($name) + if not $special then + vivy:___run_export_module($name) + end + vivy:___set_current_module($current_module) end }) - .set_name(format!("required {name}"))? + .set_name(format!("required {name} -- post"))? .exec() } diff --git a/src/Rust/vvs_lua/src/libs/set.rs b/src/Rust/vvs_lua/src/libs/set.rs deleted file mode 100644 index 2b19f0c00924c8a76b6ef15d0c2f399264d2e272..0000000000000000000000000000000000000000 --- a/src/Rust/vvs_lua/src/libs/set.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Provide a set implementation in Lua. - -use mlua::{chunk, prelude::*}; - -crate::libs::required! { "set.lua": chunk! { - local Set = {} - - function Set.print(s) - if type(s) ~= "table" then error("expected a table") end - print(Set.tostring(s)) - end - - Set.mt = { - __add = function (a, b) - if getmetatable(a) ~= "set" or getmetatable(b) ~= "set" then - error("attempt to `add' a set with a non-set value", 2) - end - local res = Set.new {} - for k in pairs(a) do res[k] = true end - for k in pairs(b) do res[k] = true end - return res - end, - - __mul = function (a, b) - if getmetatable(a) ~= "set" or getmetatable(b) ~= "set" then - error("attempt to `mul' a set with a non-set value", 2) - end - local res = Set.new {} - for k in pairs(a) do - res[k] = b[k] - end - return res - end, - - __tostring = function(set) - if type(set) ~= "table" then error("expected a table") end - local s = "{" - local sep = "" - for e in pairs(set) do - s = s .. sep .. e - sep = ", " - end - return s .. "}" - end, - - __index = function (s, k) return rawget(s, k) ~= nil end, - __newindex = function (s, k, _) rawset(s, k, true) end, - __metatable = "set", - } - - function Set.new(t) - if type(t) ~= "table" then error("expected a table") end - local set = {} - setmetatable(set, Set.mt) - for _, l in ipairs(t) do set[l] = true end - return set - end - - return Set.new -}} diff --git a/src/Rust/vvs_lua/src/libs/vivy.rs b/src/Rust/vvs_lua/src/libs/vivy.rs index 8f8bdf26f14e1ccc2d79eab9dd60d722a01dd9e8..a738639c42b67b16f817efaf159f56fbc26ad042 100644 --- a/src/Rust/vvs_lua/src/libs/vivy.rs +++ b/src/Rust/vvs_lua/src/libs/vivy.rs @@ -1,73 +1,605 @@ //! Provides the vivy runtime. -use crate::{data::VivyDataRegisterPtr, options::VivyOptionRegisterPtr, traits::TypedValue}; -use mlua::prelude::*; -use std::path::PathBuf; +use crate::{ + data::VivyDataRegisterPtr, + dsl, + func::VivyFuncRegisterPtr, + jobs::{VivyCalledJob, VivyJobRegisterPtr}, + lua_wrapper::{ + LuaAssContainerPtr, LuaAssLinePtr, LuaAssLinesPtr, LuaAssSyllabePtr, LuaAssSyllabesPtr, + LuaAssType, + }, + options::VivyOptionRegisterPtr, +}; +use mlua::{chunk, prelude::*}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + path::PathBuf, + rc::Rc, + sync::Arc, +}; +use vvs_ass::{ASSAuxTablePtr, ASSLine, ASSLines, ASSSyllabe, ASSSyllabes, ASSType}; +use vvs_utils::either; #[derive(Default, Debug)] pub struct Vivy { options: Option<VivyOptionRegisterPtr>, data: Option<VivyDataRegisterPtr>, + jobs: Option<VivyJobRegisterPtr>, + func: Option<VivyFuncRegisterPtr>, path: Vec<String>, - loaded: Vec<String>, + + variables: HashMap<String, LuaRegistryKey>, + is_in_job_evaluation: bool, + should_print_infos: bool, + main_once: bool, + + loaded: HashSet<String>, + loading: HashSet<String>, + current_module: Option<String>, } -impl Vivy { - pub fn new_with_path(path: impl IntoIterator<Item = impl AsRef<str>>) -> Self { +pub type VivyPtr = Rc<RefCell<Vivy>>; + +#[derive(Debug)] +struct VivyExecGraph { + nodes: HashSet<String>, + edges: Vec<(String, Vec<String>, Rc<LuaRegistryKey>)>, + exits: Vec<String>, +} + +macro_rules! if_let_else_chain { + (@default: $default_expr: expr) => {{ $default_expr }}; + + ($var: ident <- $expr: expr => $var_expr: expr; + @default: $default_expr: expr + ) => { + if let Ok($var) = $expr { + $var_expr + } else { + $default_expr + } + }; + + ($var_first: ident <- $expr_first: expr => $var_expr_first: expr; + $($var: ident <- $expr: expr => $var_expr: expr);+; + @default: $default_expr: expr + ) => { + if let Ok($var_first) = $expr_first { + $var_expr_first + } else { if_let_else_chain! { + $($var <- $expr => $var_expr);+; + @default: $default_expr + } } + }; +} + +/// See [Vivy::execute_job] for an example on how to use this macro. +macro_rules! match_take { + ($expr: expr => + $(; $var: ident: $ty: ident => $var_expr: expr)+ + $(; @default => $default_expr: expr)? + ) => {{ + if_let_else_chain! { + $($var <- $expr.take::<$ty>() => $var_expr);+; + $(@default: $default_expr)? + } + }}; +} + +impl VivyExecGraph { + pub fn insert( + &mut self, + destination: impl ToString, + function: Rc<LuaRegistryKey>, + src: impl IntoIterator<Item = impl ToString>, + ) -> bool { + let dest = destination.to_string(); + let src: Vec<String> = src.into_iter().map(|str| str.to_string()).collect(); + log::debug!(target: "lua", "{dest:?} <--({function:?})--- {src:?}"); + if src.iter().filter(|src| either!(self.nodes.contains(src.as_str()) => false; { + log::error!(target: "lua", "source variable '{src}' is not present in the already assigned set"); + true + })).count().ne(&0) { + return false; + } else if !self.nodes.insert(destination.to_string()) { + log::error!(target: "lua", "re-assignation of variable '{dest}' detected"); + return false; + } + self.edges.push((dest, src, function)); + true + } +} + +impl Default for VivyExecGraph { + fn default() -> Self { Self { + nodes: HashSet::from(["INIT".to_string()]), + edges: Default::default(), + exits: Default::default(), + } + } +} + +impl<'lua, I> TryFrom<(&'lua Lua, I, LuaValue<'lua>)> for VivyExecGraph +where + I: IntoIterator<Item = (usize, Option<LuaString<'lua>>, LuaValue<'lua>)>, +{ + type Error = LuaError; + + fn try_from((lua, work, ret): (&'lua Lua, I, LuaValue<'lua>)) -> Result<Self, Self::Error> { + let mut graph = Self { + exits: dsl::into_string_vec(lua, ret)?, + ..Default::default() + }; + for (idx, dest, src) in work { + let dest = dest.ok_or(LuaError::RuntimeError("".to_string()))?; + let dest = dest.to_string_lossy(); + let src = match src { + LuaValue::UserData(src) if src.is::<VivyCalledJob>() => { + src.take::<VivyCalledJob>().expect("very internal error") + } + _ => return Err(LuaError::RuntimeError("".to_string())), + }; + if !graph.insert(&dest, src.function, &src.arguments) { + return Err(LuaError::RuntimeError(format!( + "invalid instruction n°{} in main block, edge was: '{dest:?} <- {:?}'", + idx, src.arguments + ))); + } + } + let invalids = graph.exits.iter().filter(|exit| { + either!(graph.nodes.contains(exit.as_str()) => false; { + log::error!(target: "lua", "invalid exit node '{exit}', variable was never assigned"); + true + }) + }); + if invalids.count().ne(&0) { + Err(LuaError::RuntimeError(format!( + "invalid exit nodes for execution graph: {graph:#?}" + ))) + } else { + Ok(graph) + } + } +} + +impl Vivy { + /// Create a new instance of the vivy module, with a specified include path. + pub fn new_with_path(path: impl IntoIterator<Item = impl AsRef<str>>) -> VivyPtr { + Rc::new(RefCell::new(Self { path: path .into_iter() .map(|str| str.as_ref().to_string()) .collect(), ..Default::default() + })) + } + + /// Create a new default ASS element of the asked type. + pub fn new_ass_element<'lua>(&self, lua: &'lua Lua, ty: ASSType) -> LuaResult<LuaValue<'lua>> { + macro_rules! new { + ($elem: ident) => { + paste::paste! {{ + let elem = [< ASS $elem >] ::default().into_ptr(); + elem.borrow_mut().aux = self.new_ass_aux_table(ty); + Ok( [< LuaAss $elem Ptr >] ::from(elem).to_lua(lua)?) + }} + }; + } + match ty { + ASSType::Lines => new!(Lines), + ASSType::Line => new!(Line), + ASSType::Syllabes => new!(Syllabes), + ASSType::Syllabe => new!(Syllabe), + } + } + + /// Create a new default ASS element aux table of the asked type. The table is filled with the + /// default options for the said element. + pub fn new_ass_aux_table(&self, ty: ASSType) -> ASSAuxTablePtr { + self.data + .as_ref() + .map(|data| data.borrow().get_table(ty)) + .unwrap_or_default() + .into_inner() + } + + /// Pack the arguments for a job, check for correct type, coerce if needed, etc. + fn pack_job_arguments<'lua>( + &self, + lua: &'lua Lua, + input: ASSType, + args: Vec<LuaString<'lua>>, + ) -> LuaResult<Vec<LuaValue<'lua>>> { + let (vars, errs): (Vec<_>, Vec<_>) = args + .into_iter() + .map(|arg| match self.variables.get(arg.to_str()?) { + Some(var) => match lua.registry_value::<LuaValue>(var)? { + var @ LuaValue::UserData(_) => Ok(var), + var => Err(LuaError::RuntimeError(format!( + "invalid type '{}' for job argument '{}'", + var.type_name(), + arg.to_str()? + ))), + }, + None => Err(LuaError::RuntimeError(format!( + "failed to find variable '{}' in execution state", + arg.to_str()? + ))), + }) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + let mut args: Vec<LuaValue> = Default::default(); + for var in vars.into_iter().map(Result::unwrap) { + let LuaValue::UserData(var) = var else { + panic!("we should have a user data type here, got {}", var.type_name()) + }; + match input { + ASSType::Lines | ASSType::Line => match_take! { var => + ; var: LuaAssLinePtr => { args.push(var.to_lua(lua)?); continue } + ; var: LuaAssLinesPtr => { + for var in &var.into_inner().borrow().content { + args.push(LuaAssLinePtr::from(var.clone()).to_lua(lua)?); + } + continue + } + ; var: LuaAssContainerPtr => { + for var in &var.into_inner().borrow().lines.borrow().content { + args.push(LuaAssLinePtr::from(var.clone()).to_lua(lua)?); + } + continue + } + ; @default => { return Err(LuaError::RuntimeError( + "expected a line or lines, got syllabe or syllabes".to_string() + )) } + }, + ASSType::Syllabes | ASSType::Syllabe => match_take! { var => + ; var: LuaAssSyllabePtr => { args.push(var.to_lua(lua)?); continue } + ; var: LuaAssSyllabesPtr => { + for var in &var.into_inner().borrow().content { + args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); + } + continue + } + ; var: LuaAssLinePtr => { + for var in &var.into_inner().borrow().content.borrow().content { + args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); + } + continue + } + ; var: LuaAssLinesPtr => { + for line in var.into_inner().borrow().content.iter() { + for var in &line.borrow().content.borrow().content { + args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); + } + } + continue + } + ; var: LuaAssContainerPtr => { + for line in var.into_inner().borrow().lines.borrow().content.iter() { + for var in &line.borrow().content.borrow().content { + args.push(LuaAssSyllabePtr::from(var.clone()).to_lua(lua)?); + } + } + continue + } + ; @default => { return Err(LuaError::RuntimeError( + "expected a syllabe or syllabes, got line or lines".to_string() + )) } + }, + }; + } + match input { + ASSType::Lines | ASSType::Syllabes => { + log::debug!(target: "lua", "we already have flattened the input elements"); + Ok(args) + } + ASSType::Line | ASSType::Syllabe => { + log::debug!(target: "lua", "we need to pack the flattened elements into a parent element"); + let mut elem = self.new_ass_element(lua, input)?; + for arg in args { + elem = lua + .load(chunk! { local elem = $elem; elem.push($arg); return elem }) + .eval()?; + } + Ok(vec![elem]) + } + } + } + + fn unpack_job_returns<'lua>( + &self, + lua: &'lua Lua, + name: &str, + returns: ASSType, + output: impl Iterator<Item = LuaValue<'lua>>, + ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { + fn unwrap(var: LuaValue) -> LuaAnyUserData { + match var { + LuaValue::UserData(var) => var, + _ => unreachable!(), + } + } + + fn handle_user_data<'lua>( + lua: &'lua Lua, + returns: ASSType, + var: LuaAnyUserData<'lua>, + ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { + match returns { + ASSType::Lines | ASSType::Line => match_take! { var => + ; var: LuaAssLinePtr => Ok(vec![unwrap(var.to_lua(lua)?)]) + ; var: LuaAssLinesPtr => Ok(var.into_inner().borrow().content.iter().map(|line| + unwrap(LuaAssLinePtr::from(line.clone()).to_lua(lua).unwrap()) + ).collect()) + ; @default => Err(LuaError::RuntimeError( + "expected a line or lines, got syllabe or syllabes".to_string() + )) + }, + ASSType::Syllabes | ASSType::Syllabe => match_take! { var => + ; var: LuaAssSyllabePtr => Ok(vec![unwrap(var.to_lua(lua)?)]) + ; var: LuaAssSyllabesPtr => Ok(var.into_inner().borrow().content.iter().map(|line| + unwrap(LuaAssSyllabePtr::from(line.clone()).to_lua(lua).unwrap()) + ).collect()) + ; @default => Err(LuaError::RuntimeError( + "expected a syllabe or syllabes, got line or lines".to_string() + )) + }, + } + } + + let (output, errs): (Vec<_>, Vec<_>) = output + .map(|out: LuaValue<'lua>| match out { + LuaValue::UserData(out) => handle_user_data(lua, returns, out), + LuaValue::Table(outs) => { + let (outs, errs): (Vec<_>, Vec<_>) = outs + .sequence_values() + .map(|item| handle_user_data(lua, returns, item?)) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + Ok(outs.into_iter().flat_map(Result::unwrap).collect()) + } + _ => Err(LuaError::RuntimeError(format!( + "invalid return type for job '{name}', got a value of type: {}", + out.type_name() + ))), + }) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + let (output, errs): (Vec<_>, Vec<_>) = output.into_iter().partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(err); + } + let output = output.into_iter().flat_map(Result::unwrap).collect(); + + match returns { + ASSType::Line | ASSType::Syllabe => Ok(output), + ty @ ASSType::Lines | ty @ ASSType::Syllabes => { + let mut elem = self.new_ass_element(lua, ty)?; + for out in output { + elem = lua + .load(chunk! { + local elem = $elem + elem.push($out) + elem + }) + .eval()?; + } + Ok(vec![unwrap(elem)]) + } } } + + /// Execute a job with the named arguments. The types of the arguments will be checked and it + /// will be up to this function to create the apply loop if needed. The returned values will + /// also be flattened if needed. + fn execute_job<'lua>( + &mut self, + lua: &'lua Lua, + name: &str, + job: LuaFunction<'lua>, + input: ASSType, + returns: ASSType, + args: Vec<LuaString<'lua>>, + ) -> LuaResult<Vec<LuaAnyUserData<'lua>>> { + let (output, errs): (Vec<_>, Vec<_>) = self + .pack_job_arguments(lua, input, args)? + .into_iter() + .map(|arg| { + let ret = job.call::<LuaValue, LuaValue>(arg); + self.is_in_job_evaluation = true; + ret + }) + .partition(Result::is_ok); + if let Some(Err(err)) = errs.into_iter().next() { + return Err(LuaError::CallbackError { + traceback: format!("error found while executing job '{name}'"), + cause: Arc::new(err), + }); + } + self.unpack_job_returns(lua, name, returns, output.into_iter().map(Result::unwrap)) + } } impl LuaUserData for Vivy { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("___get_loaded", |_, this, ()| Ok(this.loaded.clone())); - methods.add_method("___get_path", |_, this, ()| Ok(this.path.clone())); + macro_rules! add_setter { + ($n: ident: $p: ident; + $($name: ident: $ptr: ident);+$(;)? + ) => { + add_setter! { $n: $p } + add_setter! { $($name: $ptr);+ } + }; + + ($name: ident: $ptr: ident $(;)?) => { + methods.add_method_mut(concat!("___set_", stringify!($name)), |_, this, $name: $ptr| { + this.$name = Some($name); + Ok(()) + }); + }; + } + + macro_rules! add_getter { + ($n: literal: $t: ident => $e: expr; + $($name: literal: $this: ident => $expr: expr);+$(;)? + ) => { + add_getter! { $n: $t => $e } + add_getter! { $($name: $this => $expr);+ } + }; + + ($name: literal: $this: ident => $expr: expr $(;)?) => { + methods.add_method($name, |_, $this, ()| Ok($expr)); + }; + } + + add_setter! { + options: VivyOptionRegisterPtr; + data: VivyDataRegisterPtr; + jobs: VivyJobRegisterPtr; + func: VivyFuncRegisterPtr; + } + + add_getter! { + "___get_loaded": this => this.loaded.clone(); + "___get_path": this => this.path.clone(); + "___is_in_job_evaluation_mode": this => this.is_in_job_evaluation; + } methods.add_method_mut("___append_to_path", |_, this, pattern: String| { - Ok(this.path.push(pattern)) + this.path.push(pattern); + Ok(()) }); methods.add_method_mut("___mark_loaded", |_, this, module: String| { - Ok(this.loaded.push(module)) + this.loading.remove(&module); + Ok(()) + }); + + methods.add_method_mut("___set_current_module", |_, this, module: LuaValue| { + this.current_module = match module { + LuaValue::String(module) => Some(module.to_string_lossy().to_string()), + _ => None, + }; + Ok(()) + }); + + methods.add_method("___current_module", |lua, this, ()| { + Ok(match this.current_module { + Some(ref current_module) => current_module.clone().to_lua(lua)?, + None => LuaValue::Nil, + }) + }); + + methods.add_method("___run_export_module", |lua, this, name: LuaString| { + let table = lua.create_table()?; + this.jobs + .as_ref() + .map(|jobs| jobs.borrow_mut().export(lua, &name, &table)) + .unwrap_or(Ok(()))?; + this.func + .as_ref() + .map(|func| func.borrow_mut().export(lua, &name, &table)) + .unwrap_or(Ok(()))?; + let table = dsl::into_readonly_table(lua, name.clone(), table)?; + lua.globals().raw_set(name, table)?; + + Ok(()) }); - methods.add_method_mut("___import", |lua, this, name: String| { + methods.add_method_mut( + "___execute_job", + |lua, + this, + (name, job, args, input, returns): ( + LuaString, + LuaFunction, + Vec<LuaString>, + LuaAssType, + LuaAssType, + )| { + this.execute_job( + lua, + name.to_str()?, + job, + input.into_inner(), + returns.into_inner(), + args, + ) + }, + ); + + methods.add_method_mut("___import", |lua, this, name: String| -> LuaResult<bool> { + if this.loading.contains(&name) { + return Err(LuaError::RuntimeError(format!( + "circular dependency found with module '{name}'" + ))); + } + this.loading.insert(name.clone()); this.path .iter() - .map(|pattern| PathBuf::from(pattern.replace("?", &name))) .find_map(|path| { + let path = PathBuf::from(path.replace('?', &name)); + let was_lua_file = path + .extension() + .map(|ext| ext.eq("lua")) + .unwrap_or_default(); (path.exists() && path.is_file()).then(|| { - match lua.load(&path).eval::<LuaValue>() { - Ok(module) => Some(module), - Err(err) => { - log::error!(target: "lua", "failed to load module '{name}' at location: {}\n{err}", path.to_string_lossy()); - None + let path_name = path.to_string_lossy(); + match lua.load(&path).set_name(&path_name)?.exec() { + Ok(()) => { + this.loaded.insert(name.clone()); + Ok(was_lua_file) } + Err(err) => Err(LuaError::RuntimeError(format!( + "failed to load module '{name}' at location: {path_name}\n{err}" + ))), } - })? + }) }) - .ok_or(LuaError::RuntimeError(format!( + .unwrap_or(Err(LuaError::RuntimeError(format!( "failed to find or load module named '{name}' in path: {:#?}", this.path - ))) + )))) }); - methods.add_method_mut( - "___set_options", - |_, this, options: VivyOptionRegisterPtr| Ok(this.options = Some(options)), - ); + methods.add_method_mut("___is_already_imported", |_, this, module: String| { + Ok(this.loaded.contains(&module)) + }); + + methods.add_method_mut("___switch_to_execution_mode", |_, this, ()| { + this.is_in_job_evaluation + .then(|| { + this.is_in_job_evaluation = false; + Ok(()) + }) + .unwrap_or_else(|| { + Err(LuaError::RuntimeError( + "already in execution mode, internal error".to_string(), + )) + }) + }); - methods.add_method_mut("___set_data", |_, this, data: VivyDataRegisterPtr| { - Ok(this.data = Some(data)) + methods.add_method_mut("___print_info", |_, this, ()| { + this.should_print_infos = true; + Ok(()) }); - methods.add_method_mut("___finish_setup", |lua, this, ()| { + methods.add_method_mut("___main", |lua, this, table: LuaTable| -> LuaResult<()> { + if this.main_once { + return Err(LuaError::RuntimeError( + "multiple 'main' blocks detected".to_string(), + )); + } + + // Finish setup if let Some(ref options) = this.options { let options = options.borrow(); for option in options.iter_registered() { @@ -83,86 +615,46 @@ impl LuaUserData for Vivy { data.borrow_mut().compute_cached_tables() } - Ok(()) - }); - - methods.add_method("___print_info", |lua, this, ()| { - println!(" <<< Information Report >>>"); - - let version = lua.globals().get::<_, String>("_VERSION")?; - let mut packages = crate::get_loaded(lua)?; - packages.sort(); - let packages = packages.join(", "); - println!(" # Misc"); - println!(" - version {version}"); - println!(" - packages {packages}"); - - if let Some(options) = this.options.as_ref() { - let options = options.borrow(); - - if options.iter_registered().count() >= 1 { - println!(" # Options"); - let padding = options.iter_registered().map(String::len).max(); - let padding = padding.unwrap_or_default(); - options.iter_registered().for_each(|name| { - let opt = options - .resolve(name) - .map(|opt| format!(" : {} = {opt}", opt.ty())) - .unwrap_or_else(|| { - options - .get_type(name) - .map(|ty| format!(" : {ty}")) - .unwrap_or(" : nil".to_string()) - }); - let doc = options - .get_docstring(name) - .and_then(|docstring| docstring.into_iter().next()) - .map(|line| format!(" # {line}")); - let doc = doc.unwrap_or_default(); - println!(" - option {name:padding$}{opt}{doc}"); - }); + // Print infos + if this.should_print_infos { + println!(" <<< Information Report >>>"); + println!(" # Misc"); + println!( + " - version {}", + lua.globals().get::<_, String>("_VERSION")? + ); + println!(" - packages {}", { + let mut packages = this.loaded.iter().cloned().collect::<Vec<_>>(); + packages.sort(); + let packages = packages.join(", "); + packages + }); + if let Some(options) = this.options.as_ref() { + options.borrow().print_info(); } - - let undefined: Vec<_> = options.iter_undefined_toml_options().collect(); - if !undefined.is_empty() { - println!(" # Unused Toml Options"); - let max = undefined.iter().map(|(name, _)| name.len()).max(); - let max = max.unwrap_or_default(); - undefined.into_iter().for_each(|(name, value)| { - println!(" - {name:max$} = {}", value.to_value()); - // - }); - } - } - - if let Some(data) = this.data.as_ref() { - let data = data.borrow(); - let added: Vec<_> = data - .iter_registered() - .map(|(ty, name)| (ty, name, format!("{ty}.aux.{name}"))) - .collect(); - if !added.is_empty() { - println!(" # Data"); - let padding = added.iter().map(|(_, _, name)| name.len()).max(); - let padding = padding.unwrap_or_default(); - for (ty, data_name, format_name) in added { - println!( - " - data {format_name:padding$} = {}", - data.get_table(ty) - .as_inner() - .borrow() - .get(data_name) - .expect("vivy internal error"), - ); - } + if let Some(data) = this.data.as_ref() { + data.borrow().print_info(); } - - let orphans: Vec<_> = data.get_orphan_data_declarations().collect(); - assert!(orphans.is_empty()); + this.should_print_infos = false; } + // Do the work + this.is_in_job_evaluation = true; + let table_last_idx = table.raw_len() - 1; + let (work, ret): (Vec<_>, Vec<_>) = table + .pairs::<LuaValue, LuaValue>() + .enumerate() + .flat_map(|(idx, pair)| { + let (dest, src) = pair.ok()?; + Some((idx, lua.coerce_string(dest).unwrap_or_default(), src)) + }) + .partition(|(idx, ..)| i64::try_from(*idx).unwrap() != table_last_idx); + let [(_, _, ret)] = &ret[..] else { unreachable!("invalid return statements, should only be one: {ret:#?}") }; + let exec_graph = VivyExecGraph::try_from((lua, work, ret.clone()))?; + log::error!(target: "lua", "implement execution logic for: {exec_graph:#?}"); + this.is_in_job_evaluation = false; Ok(()) - }) + }); } } diff --git a/src/Rust/vvs_lua/src/data/lua_wrapper.rs b/src/Rust/vvs_lua/src/lua_wrapper.rs similarity index 55% rename from src/Rust/vvs_lua/src/data/lua_wrapper.rs rename to src/Rust/vvs_lua/src/lua_wrapper.rs index e0cb563e5ddf2b1c42351f12634d7f94ed7ec303..63479414f919161ad03daf3cbcbd924d90f25b5b 100644 --- a/src/Rust/vvs_lua/src/data/lua_wrapper.rs +++ b/src/Rust/vvs_lua/src/lua_wrapper.rs @@ -2,22 +2,23 @@ use crate::traits::TypedValue; use mlua::prelude::*; +use std::str::FromStr; use vvs_ass::{ - ASSAuxTablePtr, ASSAuxValue, ASSLinePtr, ASSLinesPtr, ASSPositionPtr, ASSSyllabePtr, - ASSSyllabesPtr, + ASSAuxTablePtr, ASSAuxValue, ASSContainerPtr, ASSLinePtr, ASSLinesPtr, ASSPositionPtr, + ASSSyllabePtr, ASSSyllabesPtr, ASSType, }; macro_rules! wrap_ass2lua { - ($($type: ident - $(, $fields: ident -> $add_fields: expr + ($($type: ident $(-> $($trait: ident),+)? + $(: $fields: ident -> $add_fields: expr , $methods: ident -> $add_methods: expr)? );+ $(;)?) => { - $(wrap_ass2lua!(@@private $type $(; $fields -> $add_fields ; $methods -> $add_methods)?);)+ + $(wrap_ass2lua!(@@private $type $($($trait),+)? $(; $fields -> $add_fields ; $methods -> $add_methods)?);)+ }; - (@@decl $type: ident) => { + (@@decl $type: ident $($trait: ident),*) => { paste::paste! { - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, $($trait),*)] #[repr(transparent)] pub(crate) struct [< LuaAss $type >] ([< ASS $type >]); @@ -41,15 +42,15 @@ macro_rules! wrap_ass2lua { } }; - (@@private $type: ident) => { - wrap_ass2lua!(@@decl $type); + (@@private $type: ident $($trait: ident),*) => { + wrap_ass2lua!(@@decl $type $($trait),*); paste::paste! { impl LuaUserData for [< LuaAss $type >] { } } }; - (@@private $type: ident + (@@private $type: ident $($trait: ident),* ; $fields : ident -> $add_fields : expr ; $methods: ident -> $add_methods: expr) => { - wrap_ass2lua!(@@decl $type); + wrap_ass2lua!(@@decl $type $($trait),*); paste::paste! { impl LuaUserData for [< LuaAss $type >] { fn add_fields <'lua, F: LuaUserDataFields <'lua, Self>>($fields: &mut F) { $add_fields } @@ -64,6 +65,14 @@ macro_rules! add_method { add_method! { $fields => aux @ LuaAssAuxTablePtr } }; + ($methods: expr => @copy) => { + add_method! { + method $methods => "copy" (_lua, this) -> { + Ok(Self((&*this).borrow().clone().into_ptr())) + } + } + }; + ($fields: expr => $field: ident @ $wrapper: ident) => { add_method! { get $fields => $field @ $wrapper } add_method! { set $fields => $field @ $wrapper } @@ -81,36 +90,60 @@ macro_rules! add_method { Ok(()) }); }; + + (method $methods: expr => $name: literal ($lua: ident, $this: ident) -> $expr: expr) => { + $methods.add_method($name, |$lua, Self($this), ()| $expr) + }; } wrap_ass2lua! { + Type; AuxValue; - AuxTablePtr; - PositionPtr; - LinePtr, + AuxTablePtr -> Default: + _f -> {}, + m -> { + add_method! { m => @copy } + }; + PositionPtr: + _f -> {}, + m -> { + add_method! { m => @copy } + }; + // The ASS types + ContainerPtr; + LinePtr: f -> { add_method! { f => @aux } add_method! { f => position @ LuaAssPositionPtr } }, - _m -> {}; - LinesPtr, + m -> { + add_method! { m => @copy } + }; + LinesPtr: f -> { add_method! { f => @aux } add_method! { f => position @ LuaAssPositionPtr } }, - _m -> {}; - SyllabePtr, + m -> { + add_method! { m => @copy } + add_method! { method m => "push" (_lua, _this) -> { Ok(()) }} + }; + SyllabePtr: f -> { add_method! { f => @aux } add_method! { f => position @ LuaAssPositionPtr } }, - _m -> {}; - SyllabesPtr, + m -> { + add_method! { m => @copy } + }; + SyllabesPtr: f -> { add_method! { f => @aux } add_method! { f => position @ LuaAssPositionPtr } }, - _m -> {}; + m -> { + add_method! { m => @copy } + }; } impl TypedValue for LuaAssAuxValue { @@ -118,3 +151,22 @@ impl TypedValue for LuaAssAuxValue { self.0.ty() } } + +impl FromStr for LuaAssType { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(Self(s.parse().map_err(|err| format!("{err}"))?)) + } +} + +impl<'lua> TryFrom<LuaString<'lua>> for LuaAssType { + type Error = String; + + fn try_from(value: LuaString<'lua>) -> Result<Self, Self::Error> { + match value.to_str() { + Ok(bt) => Ok(Self(bt.parse().map_err(|err| format!("{err}"))?)), + _ => Err("can't build a vvs_lua::BaseType from an invalid utf8 string".to_string()), + } + } +} diff --git a/src/Rust/vvs_lua/src/options/actions.rs b/src/Rust/vvs_lua/src/options/actions.rs index 77db4215c35ff07627f4133786d84127e4a22448..ca0f4c9742512df7a70b3572a708ee32d135255f 100644 --- a/src/Rust/vvs_lua/src/options/actions.rs +++ b/src/Rust/vvs_lua/src/options/actions.rs @@ -1,4 +1,4 @@ -use crate::{traits::TypedValue, types::Type, values::Value}; +use crate::{dsl, traits::TypedValue, types::Type, values::Value}; use mlua::prelude::*; /// Structure used to register an option with either: @@ -103,24 +103,22 @@ impl<'lua> FromLua<'lua> for RegisterOptionValue { fn from_lua(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> { match lua_value { LuaValue::Table(table) => { - let value = match table.get::<_, LuaValue>("value").ok() { - None | Some(LuaValue::Nil) => None, - Some(value) => Some(Value::from_lua(value, lua)?), - }; - let ty = match table.get::<_, LuaValue>("type").ok() { - None | Some(LuaValue::Nil) => None, - Some(ty) => Some(Type::from_lua(ty, lua)?), - }; - let doc = match table.get::<_, LuaValue>("doc").ok() { - None | Some(LuaValue::Nil) => None, - Some(LuaValue::String(doc)) => Some(doc.to_string_lossy().to_string()), - Some(value) => { - return Err(LuaError::RuntimeError(format!( - "value of type '{}' can't be a docstring, expected a 'string'", - value.type_name() - ))) - } - }; + let value = dsl::table_get!(table, "value" -> { + dsl::nil_pat!() => None, + dsl::value!(value) => Some(Value::from_lua(value, lua)?), + }); + let ty = dsl::table_get!(table, "type" -> { + dsl::nil_pat!() => None, + dsl::value!(ty) => Some(Type::from_lua(ty, lua)?), + }); + let doc = dsl::table_get!(table, "doc" -> { + dsl::nil_pat!() => None, + dsl::value!(String doc) => Some(doc.to_string_lossy().to_string()), + dsl::value!(value) => return Err(LuaError::RuntimeError(format!( + "value of type '{}' can't be a docstring, expected a 'string'", + value.type_name() + ))), + }); if let (Some(value), Some(ty)) = (value.as_ref(), ty.as_ref()) { if value.ty().ne(ty) { return Err(LuaError::RuntimeError(format!( diff --git a/src/Rust/vvs_lua/src/options/register.rs b/src/Rust/vvs_lua/src/options/register.rs index f5be385a2843f211c6cbdc35bae20d20f8bae9fd..a58f7eef63876f6d5a8a7044c8a7c18554f1e60d 100644 --- a/src/Rust/vvs_lua/src/options/register.rs +++ b/src/Rust/vvs_lua/src/options/register.rs @@ -146,12 +146,8 @@ impl VivyOptionRegister { let value = value.named(name); if let Some(old) = self.registered.get(&value.name) { - if PartialEq::ne(old, &value) && !(old.is_none() || value.is_none()) { - return Err(format!("option '{}' was already registered", value.name)); - } else if (!old.is_none()) && value.is_none() { - log::warn!(target: "lua", "double register of option '{}', previous was nil, new is: {value:?}", value.name) - } else if !old.is_none() { - log::warn!(target: "lua", "double register of option '{}'", value.name) + if PartialEq::ne(old, &value) && (!old.is_none()) { + return Err(format!("option '{}' was already registered\n- old value is {old:?}\n- new value is {value:?}", value.name)); } } Self::check_type( @@ -162,6 +158,7 @@ impl VivyOptionRegister { let (ty, val_ty) = (value.ty.unwrap(), value.value.as_ref().unwrap().ty()); format!("option '{}' was declared with type '{ty}' but was defaulted with value of type '{val_ty}'", value.name) })?; + log::debug!(target: "lua", "register option {} with: {value:?}", value.name); self.registered.insert(value.name.to_string(), value); Ok(()) } @@ -176,6 +173,41 @@ impl VivyOptionRegister { _ => Some(()), } } + + /// Prints infos about the option register to stdout. + pub(crate) fn print_info(&self) { + if self.iter_registered().count() >= 1 { + println!(" # Options"); + let padding = self.iter_registered().map(String::len).max(); + let padding = padding.unwrap_or_default(); + self.iter_registered().for_each(|name| { + let opt = self + .resolve(name) + .map(|opt| format!(" : {} = {opt}", opt.ty())) + .unwrap_or_else(|| { + self.get_type(name) + .map(|ty| format!(" : {ty}")) + .unwrap_or(" : nil".to_string()) + }); + let doc = self + .get_docstring(name) + .and_then(|docstring| docstring.into_iter().next()) + .map(|line| format!(" # {line}")); + let doc = doc.unwrap_or_default(); + println!(" - option {name:padding$}{opt}{doc}"); + }); + } + + let undefined: Vec<_> = self.iter_undefined_toml_options().collect(); + if !undefined.is_empty() { + println!(" # Unused Toml Options"); + let max = undefined.iter().map(|(name, _)| name.len()).max(); + let max = max.unwrap_or_default(); + undefined.into_iter().for_each(|(name, value)| { + println!(" - unused {name:max$} = {}", value.to_value()); + }); + } + } } impl LuaUserData for VivyOptionRegister { diff --git a/src/Rust/vvs_lua/src/traits.rs b/src/Rust/vvs_lua/src/traits.rs index 91e1f21a338390da8c00901965a70502c0014a2a..0901ec534ec72b346e0e9ad31c925daf74c8d7bf 100644 --- a/src/Rust/vvs_lua/src/traits.rs +++ b/src/Rust/vvs_lua/src/traits.rs @@ -1,6 +1,7 @@ //! General traits -use crate::types::*; +use crate::{libs::Vivy, types::*}; +use vvs_ass::{ptr, ASSLine, ASSLines, ASSSyllabe, ASSSyllabes, Ptr}; /// A trait for typed values that can be used to pass informations between Lua and Rust code. pub trait TypedValue { @@ -24,10 +25,53 @@ impl TypedValue for vvs_ass::ASSAuxValue { fn ty(&self) -> Type { use vvs_ass::ASSAuxValue; match self { - ASSAuxValue::Integer(_) => Type::Base(BaseType::Number), ASSAuxValue::Floating(_) => Type::Base(BaseType::Number), + ASSAuxValue::Integer(_) => Type::Base(BaseType::Number), ASSAuxValue::Boolean(_) => Type::Base(BaseType::Boolean), ASSAuxValue::String(_) => Type::Base(BaseType::String), } } } + +/// Trait used to build an element from a single element that will be contained. +/// +/// If the trait is implemented for T, then it will be auto-implemented for [crate::Ptr<T>] if T is +/// a clonable type (it should be for ASS elements). +pub trait FromSingleElement { + type SingleElement; + + /// Instanciate an element from a single child. + fn from_single_element(vivy: &Vivy, single: Self::SingleElement) -> Self; +} + +impl<T: FromSingleElement> FromSingleElement for Ptr<T> +where + <T as FromSingleElement>::SingleElement: Clone, +{ + type SingleElement = Ptr<<T as FromSingleElement>::SingleElement>; + + fn from_single_element(vivy: &Vivy, single: Self::SingleElement) -> Self { + ptr!(FromSingleElement::from_single_element( + vivy, + (*single.borrow()).clone() + )) + } +} + +macro_rules! impl_from_single_elem { + ($from: ident => $to: ident) => { + impl FromSingleElement for paste::paste! { [< ASS $to >] } { + type SingleElement = paste::paste! { [< ASS $from >] }; + fn from_single_element(vivy: &Vivy, single: Self::SingleElement) -> Self { + Self { + position: single.position.clone(), + content: vec![ptr!(single)], + aux: vivy.new_ass_aux_table(vvs_ass::ASSType::$to), + } + } + } + }; +} + +impl_from_single_elem! { Line => Lines } +impl_from_single_elem! { Syllabe => Syllabes } diff --git a/src/Rust/vvs_lua/src/types.rs b/src/Rust/vvs_lua/src/types.rs index f4ace7ddcdc801a2348c13f3998de7a3506da2ed..38c60f82301130ae19da5a192525596c771b28b3 100644 --- a/src/Rust/vvs_lua/src/types.rs +++ b/src/Rust/vvs_lua/src/types.rs @@ -30,13 +30,27 @@ impl AsRef<str> for Type { } } +impl<'lua> TryFrom<LuaString<'lua>> for BaseType { + type Error = String; + + fn try_from(value: LuaString<'lua>) -> Result<Self, Self::Error> { + match value.to_str() { + Ok("number") => Ok(BaseType::Number), + Ok("string") => Ok(BaseType::String), + Ok("boolean") => Ok(BaseType::Boolean), + Ok(bt) => Err(format!("unknown base type: '{bt}'")), + _ => Err("can't build a vvs_lua::BaseType from an invalid utf8 string".to_string()), + } + } +} + impl<'lua> FromLua<'lua> for BaseType { fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> { match lua_value { LuaValue::String(ty) => match &ty.to_string_lossy()[..] { "number" => Ok(BaseType::Number), - "boolean" => Ok(BaseType::Boolean), "string" => Ok(BaseType::String), + "boolean" => Ok(BaseType::Boolean), ty => Err(LuaError::RuntimeError(format!( "string \"{ty}\" can't be decoded into vvs_lua::BaseType" ))), diff --git a/src/Rust/vvs_procmacro/Cargo.toml b/src/Rust/vvs_procmacro/Cargo.toml index c7239a82228f3616d2bcba45a2802edc67b44601..3e99f27c4f2c4e3f9b13b4b8108390ba1fc94874 100644 --- a/src/Rust/vvs_procmacro/Cargo.toml +++ b/src/Rust/vvs_procmacro/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true -description.workspace = true +description = "The procmacro utility crate for VVS" [lib] proc-macro = true diff --git a/src/Rust/vvs_procmacro/src/lib.rs b/src/Rust/vvs_procmacro/src/lib.rs index 50a2d6fba57c54f530808e2b7e011690da5f933c..3d267f43895febebb610de2e1e6aa77c90ff793a 100644 --- a/src/Rust/vvs_procmacro/src/lib.rs +++ b/src/Rust/vvs_procmacro/src/lib.rs @@ -33,7 +33,7 @@ pub fn derive_enum_variant_iter(input: TokenStream) -> TokenStream { enum_item .variants .into_iter() - .map(|variant| format!("{}::{}", syn_item.ident, variant.ident.to_string())) + .map(|variant| format!("{}::{}", syn_item.ident, variant.ident)) .collect::<Vec<_>>() .join(", "), )) diff --git a/src/Rust/vvs_repl/Cargo.toml b/src/Rust/vvs_repl/Cargo.toml index c03cb68ef06aaaa5e9abe19b0a69434c5c8834b1..14ec9b1ac943835066e4ad9c26b7d399af626681 100644 --- a/src/Rust/vvs_repl/Cargo.toml +++ b/src/Rust/vvs_repl/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true -description.workspace = true +description = "REPL implementation for VVS" [dependencies] log.workspace = true diff --git a/src/Rust/vvs_utils/Cargo.toml b/src/Rust/vvs_utils/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..592c9a481bf372530f58ad708e120ab58d0c0d8d --- /dev/null +++ b/src/Rust/vvs_utils/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "vvs_utils" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "Utility crate for VVS" diff --git a/src/Rust/vvs_utils/src/angles.rs b/src/Rust/vvs_utils/src/angles.rs new file mode 100644 index 0000000000000000000000000000000000000000..53424bca73c2b29483bc6bcba1bb6678334b9af6 --- /dev/null +++ b/src/Rust/vvs_utils/src/angles.rs @@ -0,0 +1,17 @@ +use core::{f32::consts::PI as PI_32, f64::consts::PI as PI_64}; + +pub fn f64_randians_clamp(rad: f64) -> f64 { + rad % (2.0 * PI_64) +} + +pub fn f64_degres_clamp(rad: f64) -> f64 { + rad % 360.0 +} + +pub fn f32_randians_clamp(rad: f32) -> f32 { + rad % (2.0 * PI_32) +} + +pub fn f32_degres_clamp(rad: f32) -> f32 { + rad % 360.0 +} diff --git a/src/Rust/vvs_utils/src/assert.rs b/src/Rust/vvs_utils/src/assert.rs new file mode 100644 index 0000000000000000000000000000000000000000..eaac3d3422d4c09b6dbe6e2890d76e1718898918 --- /dev/null +++ b/src/Rust/vvs_utils/src/assert.rs @@ -0,0 +1,15 @@ +#[macro_export] +macro_rules! assert_eq_delta { + ($a: expr, $b: expr, $delta: literal) => {{ + let (a, b, delta) = ($a, $b, $delta); + let diff = (a - b); + if !((diff <= delta && diff >= 0.0) || (-diff <= delta && diff <= 0.0)) { + panic!( + "assertion failed: not delta-equal: `abs({a} - {b}) > {delta}` \n\tleft: {}\n\tright: {}\n\tdelta: {}", + stringify!($a), + stringify!($b), + stringify!($delta) + ); + } + }}; +} diff --git a/src/Rust/vvs_utils/src/conds.rs b/src/Rust/vvs_utils/src/conds.rs new file mode 100644 index 0000000000000000000000000000000000000000..98256e0c5a19d2e900f59dc6b6f3c1c68f9c8969 --- /dev/null +++ b/src/Rust/vvs_utils/src/conds.rs @@ -0,0 +1,18 @@ +#[macro_export] +macro_rules! either { + ($cond: expr => $then: expr ; $else: expr) => {{ + if $cond { + $then + } else { + $else + } + }}; +} + +pub fn f64_epsilon_eq(a: f64, b: f64) -> bool { + (a - b).abs() <= 10E-10 +} + +pub fn f32_epsilon_eq(a: f32, b: f32) -> bool { + (a - b).abs() <= 10E-7 +} diff --git a/src/Rust/vvs_utils/src/lib.rs b/src/Rust/vvs_utils/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..fad61c684b583d598ab3a1d17c73fd5c4d8d2b75 --- /dev/null +++ b/src/Rust/vvs_utils/src/lib.rs @@ -0,0 +1,12 @@ +mod angles; +mod assert; +mod conds; +mod minmax; + +pub use angles::*; +pub use assert::*; +pub use conds::*; +pub use minmax::*; + +#[cfg(test)] +mod tests; diff --git a/src/Rust/vvs_utils/src/minmax.rs b/src/Rust/vvs_utils/src/minmax.rs new file mode 100644 index 0000000000000000000000000000000000000000..80252cee9d4c49ccfd0f801f22dc773ceb2359cf --- /dev/null +++ b/src/Rust/vvs_utils/src/minmax.rs @@ -0,0 +1,57 @@ +#[inline] +pub fn min<T: PartialOrd>(a: T, b: T) -> T { + if a < b { + a + } else { + b + } +} + +#[inline] +pub fn max<T: PartialOrd>(a: T, b: T) -> T { + if a > b { + a + } else { + b + } +} + +#[macro_export] +macro_rules! min { + ($x: expr) => { $x }; + ($x: expr, $($z: expr),+) => { ::core::cmp::min($x, $crate::min!($($z),*)) }; +} + +#[macro_export] +macro_rules! max { + ($x: expr) => { $x }; + ($x: expr, $($z: expr),+) => { ::core::cmp::max($x, $crate::max!($($z),*)) }; +} + +#[macro_export] +macro_rules! minmax { + ($($xs:expr),+) => {( + $crate::min!($($xs),+), + $crate::max!($($xs),+), + )}; +} + +#[macro_export] +macro_rules! min_partial { + ($x:expr) => { $x }; + ($x:expr, $($xs:expr),+) => { $crate::min($x, $crate::min_partial!( $($xs),+ )) }; +} + +#[macro_export] +macro_rules! max_partial { + ($x:expr) => { $x }; + ($x:expr, $($xs:expr),+) => { $crate::max($x, $crate::max_partial!( $($xs),+ )) }; +} + +#[macro_export] +macro_rules! minmax_partial { + ($($xs:expr),+) => {( + $crate::min_partial!($($xs),+), + $crate::max_partial!($($xs),+), + )}; +} diff --git a/src/Rust/vvs_utils/src/tests.rs b/src/Rust/vvs_utils/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..9b40b25d9c6e9d073bf45150acbc522714fbc4c8 --- /dev/null +++ b/src/Rust/vvs_utils/src/tests.rs @@ -0,0 +1,7 @@ +use crate::*; + +/// Test whever we have what we expect with the floating modulo in Rust. +#[test] +fn test_floating_modulo() { + assert_eq_delta!(7.4 % 6.0, 1.4, 0.1); +} diff --git a/utils/manual/ASS File Format Specification.html b/utils/manual/ASS File Format Specification.html new file mode 100644 index 0000000000000000000000000000000000000000..eb7f2464cd7f16310d04efacfe4c2287b058b6d6 --- /dev/null +++ b/utils/manual/ASS File Format Specification.html @@ -0,0 +1,2245 @@ +<html data-darkreader-mode="dynamic" data-darkreader-scheme="dark" data-lt-installed="true"><head><style class="darkreader darkreader--fallback" media="screen"></style><style class="darkreader darkreader--text" media="screen"></style><style class="darkreader darkreader--invert" media="screen">.jfk-bubble.gtx-bubble, .captcheck_answer_label > input + img, span#closed_text > img[src^="https://www.gstatic.com/images/branding/googlelogo"], span[data-href^="https://www.hcaptcha.com/"] > #icon, #bit-notification-bar-iframe, ::-webkit-calendar-picker-indicator { + filter: invert(100%) hue-rotate(180deg) contrast(90%) !important; +}</style><style class="darkreader darkreader--inline" media="screen">[data-darkreader-inline-bgcolor] { + background-color: var(--darkreader-inline-bgcolor) !important; +} +[data-darkreader-inline-bgimage] { + background-image: var(--darkreader-inline-bgimage) !important; +} +[data-darkreader-inline-border] { + border-color: var(--darkreader-inline-border) !important; +} +[data-darkreader-inline-border-bottom] { + border-bottom-color: var(--darkreader-inline-border-bottom) !important; +} +[data-darkreader-inline-border-left] { + border-left-color: var(--darkreader-inline-border-left) !important; +} +[data-darkreader-inline-border-right] { + border-right-color: var(--darkreader-inline-border-right) !important; +} +[data-darkreader-inline-border-top] { + border-top-color: var(--darkreader-inline-border-top) !important; +} +[data-darkreader-inline-boxshadow] { + box-shadow: var(--darkreader-inline-boxshadow) !important; +} +[data-darkreader-inline-color] { + color: var(--darkreader-inline-color) !important; +} +[data-darkreader-inline-fill] { + fill: var(--darkreader-inline-fill) !important; +} +[data-darkreader-inline-stroke] { + stroke: var(--darkreader-inline-stroke) !important; +} +[data-darkreader-inline-outline] { + outline-color: var(--darkreader-inline-outline) !important; +} +[data-darkreader-inline-stopcolor] { + stop-color: var(--darkreader-inline-stopcolor) !important; +}</style><style class="darkreader darkreader--variables" media="screen">:root { + --darkreader-neutral-background: #131516; + --darkreader-neutral-text: #d8d4cf; + --darkreader-selection-background: #004daa; + --darkreader-selection-text: #e8e6e3; +}</style><style class="darkreader darkreader--root-vars" media="screen"></style><style class="darkreader darkreader--user-agent" media="screen">html { + background-color: #181a1b !important; +} +html { + color-scheme: dark !important; +} +html, body, input, textarea, select, button, dialog { + background-color: #181a1b; +} +html, body, input, textarea, select, button { + border-color: #736b5e; + color: #e8e6e3; +} +a { + color: #3391ff; +} +table { + border-color: #545b5e; +} +::placeholder { + color: #b2aba1; +} +input:-webkit-autofill, +textarea:-webkit-autofill, +select:-webkit-autofill { + background-color: #404400 !important; + color: #e8e6e3 !important; +} +::-webkit-scrollbar { + background-color: #202324; + color: #aba499; +} +::-webkit-scrollbar-thumb { + background-color: #454a4d; +} +::-webkit-scrollbar-thumb:hover { + background-color: #575e62; +} +::-webkit-scrollbar-thumb:active { + background-color: #484e51; +} +::-webkit-scrollbar-corner { + background-color: #181a1b; +} +* { + scrollbar-color: #454a4d #202324; +} +::selection { + background-color: #004daa !important; + color: #e8e6e3 !important; +} +::-moz-selection { + background-color: #004daa !important; + color: #e8e6e3 !important; +}</style> +<meta http-equiv="Content-Type" content="text/html; charset=GBK"> +<meta name="Generator" content="Microsoft Word 14 (filtered)"> +<title>ASS File Format Specification</title> +<style> +<!-- + /* Font Definitions */ + @font-face + {font-family:����; + panose-1:2 1 6 0 3 1 1 1 1 1;} +@font-face + {font-family:����; + panose-1:2 1 6 0 3 1 1 1 1 1;} +@font-face + {font-family:"\@����"; + panose-1:2 1 6 0 3 1 1 1 1 1;} + /* Style Definitions */ + p.MsoNormal, li.MsoNormal, div.MsoNormal + {margin:0in; + margin-bottom:.0001pt; + text-autospace:none; + font-size:10.0pt; + font-family:"Times New Roman","serif";} +p.MsoPlainText, li.MsoPlainText, div.MsoPlainText + {margin:0in; + margin-bottom:.0001pt; + font-size:10.0pt; + font-family:"Courier New";} + /* Page Definitions */ + @page WordSection1 + {size:595.45pt 841.7pt; + margin:.75in .75in .75in .75in;} +div.WordSection1 + {page:WordSection1;} + /* List Definitions */ + ol + {margin-bottom:0in;} +ul + {margin-bottom:0in;} +--> +</style><style class="darkreader darkreader--sync" media="screen"></style> + +<meta name="darkreader" content="d5cc82de8b87b7f7a3b84c9f6672eca7"><style class="darkreader darkreader--override" media="screen">.vimvixen-hint { + background-color: #7b5300 !important; + border-color: #d8b013 !important; + color: #f3e8c8 !important; +} +::placeholder { + opacity: 0.5 !important; +} +#edge-translate-panel-body, +.MuiTypography-body1, +.nfe-quote-text { + color: var(--darkreader-neutral-text) !important; +} +gr-main-header { + background-color: #0f3a48 !important; +} +.tou-z65h9k, +.tou-mignzq, +.tou-1b6i2ox, +.tou-lnqlqk { + background-color: var(--darkreader-neutral-background) !important; +} +.tou-75mvi { + background-color: #032029 !important; +} +.tou-ta9e87, +.tou-1w3fhi0, +.tou-1b8t2us, +.tou-py7lfi, +.tou-1lpmd9d, +.tou-1frrtv8, +.tou-17ezmgn { + background-color: #0a0a0a !important; +} +.tou-uknfeu { + background-color: #231603 !important; +} +.tou-6i3zyv { + background-color: #19576c !important; +} +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); } +embed[type="application/pdf"] { filter: invert(100%) contrast(90%); }</style></head> + +<body style="text-justify-trim:punctuation" data-new-gr-c-s-check-loaded="8.905.0" data-gr-ext-installed="" lang="EN-US"> + +<div class="WordSection1"> + +<p class="MsoNormal" style="text-align:center" align="center"><b><span style="font-size:16.0pt;font-family:"Arial","sans-serif"">Sub Station Alpha +v4.00<span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">+</span> Script Format</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">1. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">General +information</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">2. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">The +[sections] of a Sub Station Alpha script</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">3. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">The line +types in a Sub Station Alpha script</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">4. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Header +lines, [Script Info] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">5. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Style +lines, [v4 Styles] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">6. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Dialogue +event lines, [Events] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">7. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Comment +lines, [Events] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">8. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Picture +event lines, [Events] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">9. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Movie event +line, [Events] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">10. </span></b><b><span style="font-family:"Arial","sans-serif"">Sound event lines, [Events] section</span></b></p> + +<p class="MsoNormal" style="margin-left:77.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">11. </span></b><b><span style="font-family:"Arial","sans-serif"">Command event lines, [Events] section</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> <b>Appendix +A: Style override codes</b></span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> + Appendix +B: Embedded font/picture encoding</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:63.8pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">This document was SSA��s format specification originally (can be +found at http://www.eswat.demon.co.uk). Updates and differences are marked red.</span></b><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +1. General Information</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The +information in this document assumes that you are familiar with the terms and +concepts used by Sub Station Alpha (SSA). These are documented in SSA's help +file, ssa.hlp which is distributed with the program, or can be downloaded +separatelyfrom http://www.eswat.demon.co.uk.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:14.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">1. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">The SSA +v4.00 script format is different to previous versions of SSA<br> +</span></b><span style="font-family:"Arial","sans-serif"">SSA v4.00 will read +scripts from older versions, but v4.00 scripts will not load into older +versions of SSA correctly.<br> +<br> +Some of the changes in the script format are intended to allow all versions of +SSA from v4.00 onwards to read any present or future SSA scripts. In +particular, the new "Format" lines allow SSA to read only the +information it recognises and discard any new information that is added in +future scripts.<br> +<br> +</span></p> + +<p class="MsoNormal" style="margin-left:14.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">2. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Scripts are +plain (DOS) text files.<br> +</span></b><span style="font-family:"Arial","sans-serif"">This means they can +be "manually" editied using any text editor, but caution must be +exercised when doing this - SSA assumes that scripts will adhere to the +"rules" set out in this document, and any errors may lead to +unpredictable results when the script is loaded into SSA.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:14.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">2. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">The script +is divided into ��ini file�� style sections<br> +</span></b><span style="font-family:"Arial","sans-serif"">However, SSA scripts +are not true Windows .ini files and you cannot do certain things you might +expect!<br> +<br> +</span></p> + +<p class="MsoNormal" style="margin-left:14.15pt;text-indent:-14.15pt"><b><span style="font-family:"Arial","sans-serif"">3. <span style="font:7.0pt "Times New Roman""> +</span></span></b><b><span style="font-family:"Arial","sans-serif"">Most lines +in each section begin with some sort of code - a "line descriptor"</span></b><span style="font-family:"Arial","sans-serif"">, to say what information is held in it. +The descriptor is terminated by a colon.</span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><b><span style="font-family:"Arial","sans-serif"">3. The information fields in +each line are separated by a commas.</span></b></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> This makes it +illegal to use commas in character names and style names (SSA prevents you +putting commas in these). It also makes it quite easy to load chunks of an SSA +script into a spreadsheet as a CSV file, and chop out columns of information +you need for another subtitling program.</span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><b><span style="font-family:"Arial","sans-serif"">4. SSA does not care what order +events are entered in.</span></b></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> They could be +entered in complete reverse order, and SSA would still play everything +correctly in the right order ie. you cannot assume that each dialogue line is +in chronological order in the script file.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><b><span style="font-family:"Arial","sans-serif"">5. Incorrectly formatted lines +are ignored.<br> +</span></b><span style="font-family:"Arial","sans-serif"">SSA will discard any +lines it doesn't understand, and give a warning after the script has loaded +giving the number of lines it discarded.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><b><span style="font-family:"Arial","sans-serif"">6. Lines cannot be split</span></b></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> Each entry in +a script contains all its information in a single line.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><b><span style="font-family:"Arial","sans-serif"">7. If unknown styles are used in +the script, the *Default style will be used.</span></b></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> For example, +if lines have been pasted in from another script, without the corresponding +Style information then when SSA plays the script, the Default style settings +will be used.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">8. If +a Style specifies a font which is not installed, then Arial will be used +instead.</span></b></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> This can +happen with scripts which you did not create yourself - the original authors +may have fonts installed which you don't have.</span></p> + +<p class="MsoNormal" style="margin-left:13.5pt;text-indent:-13.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">2. The sections in a +Sub Station Alpha script</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><span style="text-decoration:none"> </span></span></u></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">[Script +Info]</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This section +contains headers and general information about the script.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The line that +says ��[Script Info]�� <b>must</b> be the first line in a v4 script.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">[v4 +Styles]</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This section +contains all Style definitions required by the script. Each ��Style�� used by +subtitles in the script should be defined here.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">ASS +uses [v4 Styles+]</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">[Events]</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This section +contains all the events for the script - all the subtitles, comments, pictures, +sounds, movies and commands. Basically, everything that you see in Sub Station +Alpha��s main-screen ��grid�� is in this section.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">[Fonts]</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This section +contains text-encoded font files, if the user opted to embed non-standard fonts +in the script. Only truetype fonts can be embedded in SSA scripts. Each font +file is started with a single line in the format:<br> +<br> +</span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">fontname: +<name of file></span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The word ��<b>fontname</b>�� +must be in lower case (upper case will be interpretted as part of a +text-encoded file). </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""><name +of file></span></b><span style="font-family:"Arial","sans-serif""> is the +file name that SSA will use when saving the font file. It is:</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">the name of +the original truetype font,</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">plus an +underscore,</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">plus an +optional ��B�� if Bold,</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">plus an +optional ��I�� if Italic,</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">plus a number +specifying the font encoding (character set),</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">plus ��.ttf��</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Eg.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">fontname: +chaucer_B0.ttf</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">fontname: +comic_0.ttf</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The fontname +line is followed by lines of printable characters, representing the binary +values which make up the font file. Each line is 80 characters long, except the +last one which may be less.<br> +<br> +The conversion from binary to printable characters is a form of Uuencoding, the +details of this encoding is contained in "Appendix B", below.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">[Graphics]</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This sections +contains text-encoded graphic files, if the user opted to embed any pictures +they used in the script. The binary picture files are text-encoded, which is +inefficient, but ensures that SSA scripts can still be handled by any text editor, +if required.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Each graphic +file is started with a single line in the format:</span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">filename: +<name of file></span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The word ��<b>filename</b>�� +must be in lower case (upper case will be interpreted as part of a text-encoded +file). <br> + </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""><name +of file></span></b><span style="font-family:"Arial","sans-serif""> is the +file name that SSA will use when saving the picture file. It will match the +filename of a picture used in the script.<br> +<br> +SSA saves any files found in the script in a subdirectory off SSA's program +directory, "Pictures"</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">eg. +c:\program files\Sub Station Alpha v4.00\Pictures. SSA will attempt to load +files using the paths specified in the script, but if they are not found, it +will look in the "Pictures" subdirectory for them.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The fontname +line is followed by lines of printable characters, fontrepresenting the binary +values which make up the picture font file - format is as per embedded font +files.</span></p> + +<span style="font-size:10.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">3. The line types in +a Sub Station Alpha script</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This briefly +describes each of the line types that can appear in a Sub Station Alpha Script. +Full details of the information held in each line type is in the next chapter.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">!:</span></b><span style="font-family: +"Arial","sans-serif""> + This +is a comment used in the script file only. It is not visible when you +load the +script into SSA.</span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Title:</span></b><span style="font-family:"Arial","sans-serif""> + This +is a description of the script</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Original +Script:</span></b><span style="font-family:"Arial","sans-serif""> The +original author(s) of the script</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Original +Translation: </span></b><span style="font-family:"Arial","sans-serif"">(optional) +The original translator of the dialogue</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Original Editing: </span></b><span style="font-family:"Arial","sans-serif"">(optional) The original script +editor(s), typically whoever took the raw translation and turned it into +idiomatic english and reworded for readability.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Original +Timing:</span></b><span style="font-family:"Arial","sans-serif""> (optional) +Whoever timed the original script</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Synch Point:</span></b><span style="font-family:"Arial","sans-serif""> (optional) +Description of where in the video the script should begin playback.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Script Updated By:</span></b><span style="font-family:"Arial","sans-serif""> (optional) Names of +any other subtitling groups who edited the original script.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Update Details:</span></b><span style="font-family:"Arial","sans-serif""> The +details of any updates to the original script made by other subtilting groups.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">ScriptType:</span></b><span style="font-family:"Arial","sans-serif""> This +is the SSA script format version eg. "V3.00".</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Collisions:</span></b><span style="font-family:"Arial","sans-serif""> This +determines how subtitles are moved, when preventing onscreen collisions</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">PlayResY:</span></b><span style="font-family:"Arial","sans-serif""> This +is the height of the screen used by the authors when playing the script.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">PlayResX:</span></b><span style="font-family:"Arial","sans-serif""> This +is the width of the screen used by the authors when playing the script.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">PlayDepth: </span></b><span style="font-family:"Arial","sans-serif"">This is the colour depth used by the +authors when playing the script.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Timer:</span></b><span style="font-family:"Arial","sans-serif""> This +is the Timer Speed for the script, as a percentage.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + eg. +"100.0000" is exactly 100%.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + The +timer speed is a time multiplier applied to SSA's clock to provide a +ramp time.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><b><span style="font-family:"Arial","sans-serif"">Style:</span></b><span style="font-family:"Arial","sans-serif""> + This +is a Style definition, used to format text displayed by the script.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Dialogue:</span></b><span style="font-family:"Arial","sans-serif""> This +is a Dialogue event, ie. Some text to display.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Comment:</span></b><span style="font-family:"Arial","sans-serif""> This is a +"comment" event.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + This +contains the same information as a Dialogue, Picture, Sound, Movie, or +Command +event, but it is ignored during script playback.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Picture:</span></b><span style="font-family:"Arial","sans-serif""> This +is a "picture" event, which means SSA will display the specified +.bmp, .jpg, .gif, .ico or .wmf graphic.</span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><b><span style="font-family:"Arial","sans-serif"">Sound:</span></b><span style="font-family:"Arial","sans-serif""> + This +is a "sound" event, which means SSA will play the specified .wav +file.</span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><b><span style="font-family:"Arial","sans-serif"">Movie:</span></b><span style="font-family:"Arial","sans-serif""> + This +is a "movie" event, which means SSA will play the specified .avi +file.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Command:</span></b><span style="font-family:"Arial","sans-serif""> This +is a "command" event, which means SSA will execute the specified +program as a background task.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">4. Header lines, +[Script Info] section</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><span style="text-decoration:none"> </span></span></u></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">;</span></b><span style="font-family: +"Arial","sans-serif""> + Semicolon. +Any text can follow the semicolon</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + This +is a comment used in the script file only. It is not visible when you +load the +script into SSA. The semicolon <b>must</b> be the first character in the line. +This replaces the <b>!:</b> line type used in previous script versions.</span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Title:</span></b><span style="font-family:"Arial","sans-serif""> + This +is a description of the script. If the original author(s) did not +provide this +information then <untitled> is automatically substituted.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Original Script:</span></b><span style="font-family:"Arial","sans-serif""> The +original author(s) of the script. If the original author(s) did not provide +this information then <unknown> is automatically substituted.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Original Translation: </span></b><span style="font-family:"Arial","sans-serif"">(optional) The original translator of +the dialogue. This entry does not appear if no information was entered by the +author.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Original Editing: </span></b><span style="font-family:"Arial","sans-serif"">(optional) The original script +editor(s), typically whoever took the raw translation and turned it into +idiomatic english and reworded for readability. This entry does not appear if +no information was entered by the author.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Original Timing:</span></b><span style="font-family:"Arial","sans-serif""> (optional) +Whoever timed the original script. This entry does not appear if no information +was entered by the author.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Synch Point:</span></b><span style="font-family:"Arial","sans-serif""> (optional) +Description of where in the video the script should begin playback. This entry +does not appear if no information was entered by the author.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Script Updated By:</span></b><span style="font-family:"Arial","sans-serif""> (optional) Names of +any other subtitling groups who edited the original script. This entry does not +appear if subsequent editors did not enter the information.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Update Details:</span></b><span style="font-family:"Arial","sans-serif""> The +details of any updates to the original script - made by other subtitling +groups. This entry does not appear if subsequent editors did not enter any +information.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Script Type:</span></b><span style="font-family:"Arial","sans-serif""> This +is the SSA script format version eg. "V4.00". It is used by SSA to +give a warning if you are using a version of SSA older than the version that +created the script.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">ASS version is ��V4.00+��.</span></b></span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Collisions:</span></b><span style="font-family:"Arial","sans-serif""> This +determines how subtitles are moved, when automatically preventing onscreen +collisions.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + If +the entry says "<b>Normal</b>" then SSA will attempt to position +subtitles in the position specified by the "margins". However, +subtitles can be shifted vertically to prevent onscreen collisions. With +"normal" collision prevention, the subtitles will "stack +up" one above the other - but they will always be positioned as close the +vertical (bottom) margin as possible - filling in "gaps" in other subtitles +if one large enough is available.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + If +the entry says "<b>Reverse</b>" then subtitles will be shifted +upwards to make room for subsequent overlapping subtitles. This means the +subtitles can nearly always be read top-down - but it also means that the first +subtitle can appear half way up the screen before the subsequent overlapping +subtitles appear. It can use a lot of screen area.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">PlayResY:</span></b><span style="font-family:"Arial","sans-serif""> This +is the height of the screen used by the script's author(s) when playing the +script. SSA v4 will automatically select the nearest enabled setting, if you +are using Directdraw playback.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">PlayResX:</span></b><span style="font-family:"Arial","sans-serif""> This +is the width of the screen used by the script's author(s) when playing the +script. SSA will automatically select the nearest enabled, setting if you are +using Directdraw playback.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">PlayDepth: </span></b><span style="font-family:"Arial","sans-serif"">This is the colour depth used by the +script's author(s) when playing the script. SSA will automatically select the +nearest enabled setting if you are using Directdraw playback.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">Timer:</span></b><span style="font-family:"Arial","sans-serif""> + This +is the Timer Speed for the script, as a percentage.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + eg. +"100.0000" is exactly 100%. It has four digits following the decimal +point.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + The +timer speed is a time multiplier applied to SSA's clock to stretch or +compress +the duration of a script. A speed greater than 100% will reduce the +overall +duration, and means that subtitles will progressively appear sooner and +sooner. +A speed less than 100% will increase the overall duration of the script +means +subtitles will progressively appear later and later (like a positive +ramp +time).<br> +<br> +The stretching or compressing only occurs during script playback - this value +does not change the actual timings for each event listed in the script.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + Check +the SSA user guide if you want to know why "Timer Speed" is more +powerful than "Ramp Time", even though they both achieve the same +result.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">WrapStyle: Defines +the default wrapping style. </span></b></p> + +<p class="MsoNormal" style="margin-left:99.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">0: smart wrapping, lines are evenly broken</span></b></p> + +<p class="MsoNormal" style="margin-left:99.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">1: end-of-line word wrapping, only \N breaks</span></b></p> + +<p class="MsoNormal" style="margin-left:99.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">2: no word wrapping, \n \N both breaks</span></b></p> + +<p class="MsoNormal" style="margin-left:99.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">3: same as 0, but lower line gets wider.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">5. Style Lines, [v4<span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">+</span> Styles] section</span></u></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Styles define +the appearance and position of subtitles. All styles used by the script are are +defined by a Style line in the script.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Any of the +the settings in the Style, (except shadow/outline type and depth) can +overridden by control codes in the subtitle text.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The fields +which appear in each Style definition line are named in a special line with the +line type ��Format:��. The Format line must appear before any Styles - because it +defines how SSA will interpret the Style definition lines. The field names +listed in the format line must be correctly spelled! The fields are as follows:</span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Name, +Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, +Bold, Italic, <span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Underline, StrikeOut, ScaleX, ScaleY, +Spacing, Angle,</span> BorderStyle, Outline, Shadow, Alignment, MarginL, +MarginR, MarginV, AlphaLevel, Encoding</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The format +line allows new fields to be added to the script format in future, and yet +allow old versions of the software to read the fields it recognises - even if +the field order is changed.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Field 1: <b>Name</b>. +The name of the Style. Case sensitive. Cannot include commas.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Field 2: <b>Fontname</b>. +The fontname as used by Windows. Case-sensitive.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Field 3: <b>Fontsize</b>.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 4: <b>PrimaryColour.</b> +A long integer BGR (blue-green-red) value. ie. the byte order in the +hexadecimal equivelent of this number is BBGGRR</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> This +is the colour that a subtitle will normally appear in.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 5: <b>SecondaryColour.</b> +A long integer BGR (blue-green-red) value. ie. the byte order in the +hexadecimal equivelent of this number is BBGGRR</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> This +colour may be used instead of the Primary colour when a subtitle is +automatically shifted to prevent an onscreen collsion, to distinguish the +different subtitles.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 6: <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">OutlineColor</span> (<s>TertiaryColour</s></b>). A long +integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal +equivelent of this number is BBGGRR</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> This +colour may be used instead of the Primary or Secondary colour when a subtitle +is automatically shifted to prevent an onscreen collsion, to distinguish the +different subtitles.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 7: <b>BackColour</b>. +This is the colour of the subtitle outline or shadow, if these are used. A long +integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal +equivelent of this number is BBGGRR.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 4-7: The color +format contains the alpha channel, too. (AABBGGRR)</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 8: <b>Bold</b>. +This defines whether text is bold (true) or not (false). -1 is True, 0 is +False. This is independant of the Italic attribute - you can have have text +which is both bold and italic.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 9: <b>Italic</b>. +This defines whether text is italic (true) or not (false). -1 is True, 0 is +False. This is independant of the bold attribute - you can have have text which +is both bold and italic.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 9.1: Underline. [-1 +or 0]</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 9.2: Strikeout. [-1 +or 0]</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 9.3: ScaleX. +Modifies the width of the font. [percent]</span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 9.4: ScaleY. +Modifies the height of the font. [percent]</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 9.5: Spacing. +Extra space between characters. [pixels]</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 9.6: Angle. +The origin of the rotation is defined by the alignment. Can be a floating point +number. [degrees]</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Field 10: <b>BorderStyle</b>. +1=Outline + drop shadow, 3=Opaque box</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 11: <b>Outline.</b> +If BorderStyle is 1, then this specifies the width of the outline around +the text, in pixels.<br> +Values may be 0, 1, 2, 3 or 4. </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 12: <b>Shadow.</b> +If BorderStyle is 1, then this specifies the depth of the drop shadow +behind the text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is +always used in addition to an outline - SSA will force an outline of 1 pixel if +no outline width is given.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 13: <b>Alignment</b>. +This sets how text is "justified" within the Left/Right onscreen +margins, and also the vertical placing. Values may be 1=Left, 2=Centered, +3=Right. Add 4 to the value for a "Toptitle". Add 8 to the value for +a "Midtitle".<br> +eg. 5 = left-justified toptitle</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 13: Alignment, +but after the layout of the numpad (1-3 sub, 4-6 mid, 7-9 top).</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 14: <b>MarginL</b>. +This defines the Left Margin in pixels. It is the distance from the left-hand +edge of the screen.The three onscreen margins (MarginL, MarginR, MarginV) +define areas in which the subtitle text will be displayed.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 15: <b>MarginR</b>. +This defines the Right Margin in pixels. It is the distance from the <b>right-hand</b> +edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) +define areas in which the subtitle text will be displayed.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 16: <b>MarginV</b>. +This defines the vertical Left Margin in pixels.<br> +For a <b>subtitle</b>, it is the distance from the <b>bottom</b> of the screen.<br> +For a <b>toptitle</b>, it is the distance from the <b>top</b> of the screen.<br> +For a <b>midtitle</b>, the value is ignored - the text will be vertically +centred<br> + </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Field 17: <b>AlphaLevel.</b> +This defines the transparency of the text. SSA does not use this yet.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field +17: Not present in ASS.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 18: <b>Encoding. +</b>This specifies the font character set or encoding and on multi-lingual +Windows installations it provides access to characters used in multiple than +one languages. It is usually 0 (zero) for English (Western, ANSI) Windows.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">When the file is Unicode, this field is useful during file +format conversions.</span></b></span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<span style="font-size:10.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">5. Dialogue event +lines, [Events] section</span></u></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">These contain +the subtitle text, their timings, and how it should be displayed.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The fields +which appear in each Dialogue line are defined by a <b>Format:</b> line, which +must appear before any events in the section. The format line specifies how SSA +will interpret all following Event lines. The field names must be spelled +correctly, and are as follows:</span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">Marked, +Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The last +field will always be the Text field, so that it can contain commas. The format +line allows new fields to be added to the script format in future, and yet +allow old versions of the software to read the fields it recognises - even if +the field order is changed.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 1: <b>Marked<br> +</b>Marked=0 means the line is not shown as "marked" in SSA.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> Marked=1 +means the line is shown as "marked" in SSA.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Field 1: Layer +(any integer)</span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Subtitles +having different layer number will be ignored during the collusion detection.</span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Higher +numbered layers will be drawn over the lower numbered. </span></b></p> + +<p class="MsoNormal"><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 2: <b>Start</b><br> +Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. +This is the time elapsed during script playback at which the text will appear +onscreen. Note that there is a <b>single</b> digit for the hours!</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 3: <b>End</b><br> +End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This +is the time elapsed during script playback at which the text will disappear offscreen. +Note that there is a <b>single</b> digit for the hours!</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 4: <b>Style<br> +</b>Style name. If it is "Default", then your <b>own</b> *Default +style will be subtituted.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> However, +the Default style used by the script author IS stored in the script even though +SSA ignores it - so if you want to use it, the information is there - you could +even change the Name in the Style definition line, so that it will appear in +the list of "script" styles.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 5: <b>Name<br> +</b>Character name. This is the name of the character who speaks the dialogue. +It is for information only, to make the script is easier to follow when +editing/timing.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 6: <b>MarginL</b><br> +4-figure Left Margin override. The values are in pixels. All zeroes means the +default margins defined by the style are used.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 7: <b>MarginR</b><br> +4-figure Right Margin override. The values are in pixels. All zeroes means the +default margins defined by the style are used.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 8: <b>MarginV</b><br> +4-figure Bottom Margin override. The values are in pixels. All zeroes means the +default margins defined by the style are used.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 9: <b>Effect</b><br> +Transition Effect. This is either empty, or contains information for one of the +three transition effects implemented in SSA v4.x</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> The +effect names are case sensitive and must appear exactly as shown. The effect +names do <b>not</b> have quote marks around them.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b>"Karaoke"</b> +means that the text will be successively highlighted one word at a time.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Karaoke as an effect type is obsolete.</span></b></span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b>"Scroll +up;y1;y2;delay<span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">[;fadeawayheight]</span>"</b>means +that the text/picture will scroll up the screen. The parameters after the words +"Scroll up" are separated by semicolons.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> The +y1 and y2 values define a vertical region on the screen in which the text will +scroll. The values are in pixels, and it doesn't matter which value (top or +bottom) comes first. If the values are zeroes then the text will scroll up the +full height of the screen.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> The +delay value can be a number from 1 to 100, and it slows down the speed of the +scrolling - zero means no delay and the scrolling will be as fast as possible.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b>��Banner;delay�� +</b>means that text will be forced into a single line, regardless of length, +and scrolled from right to left accross the screen.<br> +<br> +The delay value can be a number from 1 to 100, and it slows down the speed of +the scrolling - zero means no delay and the scrolling will be as fast as +possible.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">"Scroll down;y1;y2;delay[;fadeawayheight]"</span></b></span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">��Banner;delay[;lefttoright;fadeawaywidth]��</span></b></span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> lefttoright +0 or 1. This field is optional. Default value is 0 to make it backwards +compatible.</span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> When +delay is greater than 0, moving one pixel will take (1000/delay) second. </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> (WARNING: +Avery Lee��s ��subtitler�� plugin reads the ��Scroll up�� effect parameters as delay;y1;y2)</span></b></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><br> +fadeawayheight and fadeawaywidth parameters can be used to make the scrolling +text at the sides transparent.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif"">Field 10: <b>Text</b><br> +Subtitle Text. This is the actual text which will be displayed as a subtitle +onscreen. Everything after the 9th comma is treated as the subtitle text, so it +can include commas.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> The +text can include \n codes which is a line break, and can include Style Override +control codes, which appear between braces { }.</span></p> + +<p class="MsoNormal" style="margin-left:49.5pt;text-indent:-49.5pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">6. Comment event +lines, [Events] section</span></u></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">These can +contain the same information as any of the other event line types, but they +will be ignored when the script is played.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="margin-left:14.15pt;text-align:center; +text-indent:-14.15pt" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">7. + Picture event lines, [Events] section</span></u></b></p> + +<p class="MsoNormal"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><span style="text-decoration:none"> </span></span></u></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">These contain +the same information as Dialogue events, but Field 10 contains the full path +and filename of the picture to display, instead of subtitle text.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The Style +specified is ignored. The "scroll up" transition effect can be used +for picture events.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The Left and +Vertical Margin Overrides specify the bottom-left corner position of the +picture. A left margin of all zeroes means that the picture will be +horizontally centered. A vertical margin of all zeroes means that the picture +will be vertically centered. </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="margin-left:14.15pt;text-align:center; +text-indent:-14.15pt" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">8. + Sound event lines, [Events] section</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><span style="text-decoration:none"> </span></span></u></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">These contain +the same information as Dialogue events, but Field 10 contains the full path +and filename of the wav file to play, instead of subtitle text.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The Style and +margins are ignored. The End time is also ignored - the wav will play until it +finishes, or until another wav file is played.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">If an avi +movie is played at the same time as a wav is already playing, then any sound in +the avi will not be heard. Similarly, if a wav starts playing when an avi movie +with sound is already playing then the wav will not be heard.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:63.0pt;text-indent:-63.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="margin-left:14.15pt;text-align:center; +text-indent:-14.15pt" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">9. + Movie event lines, [Events] section</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><span style="text-decoration:none"> </span></span></u></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">These contain +the same information as Dialogue events, but Field 10 contains the full path +and filename of the avi file to play, instead of subtitle text.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The Style is +ignored. Transition effects are ignored.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The End time +specifies when the movie picture will disappear - but if th eavi file lasts +longer, then the sound will continue to be heard.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The Left and +vertical Margin Overrides specify the TOP-LEFT corner position of the picture +(unlike picture events). A left margin of all zeroes means that the picture +will be horizontally centered. a vertical margin of all zeroes means that the +picture will be verticall centered. </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">If an avi +movie is played at the same time as a wav is already playing, then any sound in +the avi will not be heard. Similarly, if a wav starts playing when an avi movie +with sound is already playing then the wav will not be heard.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="margin-left:14.15pt;text-align:center; +text-indent:-14.15pt" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">10. +Command event lines, [Events] section</span></u></b></p> + +<p class="MsoNormal" style="text-align:center" align="center"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">These contain +the same information as Dialogue events, but Field 10 contains the full path +and filename of the program to execute, instead of subtitle text.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The Style is +ignored. The margins are ignored. Transition effects are ignored. The End time +is also ignored - the program will execute until it ends, or is +"manually" closed.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">There are +also internal SSA commands which can appear in SSA scripts - the +"SSA:Pause", ��SSA:Wait for trigger�� command events, and genlock +control commands. These all begin with "SSA:"</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The SSA:Pause +command has the same effect as pressing "P" during script playback. +It is useful as a second "synch point" to resume subtitling after +switching sides of a laserdisk.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The ��SSA:Wait +for audio trigger�� command has the same effect as pressing "P" during +script playback, but pausing is automatically cancelled if the audio input to +the computer exceeds a specified ��trigger�� level. It is useful as a second +"synch point" to resume subtitling after switching sides of a +laserdisk. The audio triggering can be overridden to resume playback - by +pressing "P".</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Audio +triggering "times out" after 10 minutes - If no audio peak of +sufficient magnitude is received, and "P" is not pressed within 10 +minutes - then playback will resume anyway.</span></p> + +<b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></u></b> + +<p class="MsoNormal" style="text-align:center" align="center"><b><u><span style="font-size:12.0pt;font-family:"Arial","sans-serif"">Appendix A: Style +override codes</span></u></b></p> + +<p class="MsoNormal"><b><span style="font-size:12.0pt;font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">This is a +reference which may be useful for those of you who wish to learn the style +override codes, so you can type them in manually without using the +"override style" dialogue box.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">All Override +codes appear within braces { } except the newline \n and \N codes.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">All override +codes are always preceded by a backslash \</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Several +overrides can be used within one set of braces.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Each override +affects all text following the override. To apply an override only to selected +text, you need a second "cancelling" override after the selected +text, to "undo" the effect of the first override.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Some +overrides automatically apply to ALL the text - currently this is just +alignment overrides, but more may be added later (eg. Shadow/outline depth +overrides).</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">\n</span></b><span style="font-family:"Arial","sans-serif""> + New +line (carriage return)<br> + + \n +is ignored by SSA if ��smart-wrapping�� is enabled</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> + <b>eg. +This is the first line\nand this is the second<br> +<br> +\N + </b>New +line (carriage return). This is used by SSA instead of \n if </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> + ��smart-wrapping�� +is enabled.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">\b</span></b><span style="font-family: +"Arial","sans-serif""><0 or 1> \b1 +makes the text bold. \b0 forces non-bold text.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + eg. +There is a {\b1}bold {\b0}word here</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + <span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">When this parameter is greater than 1, it will be used as the +weight of the font. (400 = Normal, 700 = Bold, note: most fonts will quantize +to 2 or 3 levels of thickness)</span></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">\i</span></b><span style="font-family:"Arial","sans-serif""><0 or 1> \i1 +makes the text italic. \i0 forces non-italic text.</span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> + eg. +There is an {\i1}italicised {\i0}word here</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\u<0 or 1> underline</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\s<0 +or 1> strikeout</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\bord<width> border</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\shad<depth> shadow</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\be<0 +or 1> blur +edges</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">\fn</span></b><span style="font-family: +"Arial","sans-serif""><font name> <font +name> specifies a font which you have installed in Windows. This is case +sensitive.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + eg. +Here is some {\fnCourier New}fixed space text</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + If +you use a font name that doesn't exist, then Arial will be used instead.<br> +<br> +</span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">\fs</span></b><span style="font-family:"Arial","sans-serif""><font size> <font +size> is a number specifying a font point size.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> + <b>eg. +{\fs16}This is small text. {\fs28}This is large text<br> +<br> +</b></span></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\fsc<x +or y><percent> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <x +or y> x scales horizontally, y scales vertically</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <percent> +</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\fsp<pixels +> <pixels> +changes the distance between letters. (default: 0)</span></b></p> + +<p class="MsoNormal" style="margin-left:99.25pt;text-indent:-90.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\fr[<x/y/z>]<degrees></span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <degrees> +sets the rotation angle around the x/y/z axis.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + \fr +defaults to \frz. </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">\fe</span></b><span style="font-family:"Arial","sans-serif""><charset> <charset> +is a number specifying the character set (font encoding)</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">\c&H</span></b><span style="font-family:"Arial","sans-serif""><bbggrr><b>& </b><bbggrr> +is a hexadecimal RGB value, but in reverse order. Leading zeroes are not +required.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + eg. +{\c&HFF&}This is pure, full intensity red</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\c&HFF00&}This is pure, full intensity Green</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\c&HFF0000&}This is pure, full intensity Blue</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\c&HFFFFFF&}This is White</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\c&HA0A0A&}This is dark grey</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + <span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\1c&Hbbggrr&, \2c&Hbbggrr&, +\3c&Hbbggrr&, \4c&Hbbggrr& to set specific colors.</span></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + \1a&Haa&, +\2a&Haa&, \3a&Haa&, \4a&Haa& to set specific +alpha +channels.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + \alpha +defaults to \1a</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">\a</span></b><span style="font-family: +"Arial","sans-serif""><alignment> <alignment> +is a number specifying the onscreen alignment/positioning of a subtitle.</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + A +value of 1 specifies a left-justified subtitle</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + A +value of 2 specifies a centered subtitle</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + A +value of 3 specifies a right-justified subtitle</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + Adding +4 to the value specifies a "Toptitle"</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + Adding +8 to the value specifies a "Midtitle"</span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> + <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">0 or nothing resets to the style default (which is usually 2)</span></b></span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + eg. +{\a1}This is a left-justified subtitle</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\a2}This is a centered subtitle</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\a3}This is a right-justified subtitle</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\a5}This is a left-justified toptitle</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> + +{\a11}This is a right-justified midtitle</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + Only +the first appearance counts.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\an<alignment> numpad +layout</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + Only +the first appearance counts.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family:"Arial","sans-serif"">\k</span></b><span style="font-family: +"Arial","sans-serif""><duration> <duration> +is the amount of time that each section of text is highlighted for in a +dialogue event with the Karaoke effect. The durations are in hundredths of +seconds.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> + eg. +{\k94}This {\k48}is {\k24}a {\k150}karaoke {\k94}line</span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + \k<duration> +highlight by words</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + \kf +or \K<duration> fill up from left to right</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + \ko<duration> +outline highlighting from left to right</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\q<num> + + +<num> +wrapping style</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family:"Arial","sans-serif"">\r<span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">[<style>]</span> </span></b><span style="font-family: +"Arial","sans-serif"">This cancels all previous style overrides in a line</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> + <b><span style="color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><style> Restores to <style> instead of the +dialogue line default.</span></b></span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Any style modifier followed +by no recognizable parameter resets to the default.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><u><span style="font-size: 12pt; font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Functions:</span></u></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\t([<t1>, +<t2>, ] [<accel>,] <style modifiers>)</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="" lang="HU"> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <t1>, +<t2> Animation beginning, ending time offset [ms] (optional)</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <accel> +Modifies the linearity of the transformation (optional)</span></b></p> + +<p class="MsoNormal" style="margin-left:127.6pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:127.6pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">The following calculation is performed to get the +coefficient needed to interpolate between the given style modifiers: +pow((t-t1)/(t2-t1), accel), where t is the time offset for the subtitle.</span></b></p> + +<p class="MsoNormal" style="margin-left:127.6pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:127.6pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">The meaning of <accel>:</span></b></p> + +<p class="MsoNormal" style="margin-left:148.85pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">1: the transformation is linear</span></b></p> + +<p class="MsoNormal" style="margin-left:148.85pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">between 0 and 1: will start fast and slow down</span></b></p> + +<p class="MsoNormal" style="margin-left:148.85pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">greater than 1: will start slow and get faster</span></b></p> + +<p class="MsoNormal" style="margin-left:148.85pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:148.85pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">As an example, using 2 will make growing the +letters (by {\fscx200\fscy200}) look linear rather than slowering.</span></b></p> + +<p class="MsoNormal" style="margin-left:148.85pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <style +modifiers>Any style modifier which can be animated:</span></b></p> + +<p class="MsoNormal" style="margin-left:120.5pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\c,\1-4c,\alpha,\1-4a,\fs,\fr,\fscx,\fscy,\fsp,\bord,\shad,\clip +(only the rectangular \clip)</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\move(<x1>, +<y1>, <x2>, <y2>[, <t1>, <t2>])</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <x1>, +<y1> The coordinate to start at.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <x2>, +<y2> The coordinate to end at.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <t1>, +<t2> Animation beginning, ending time offset [ms] (optional)</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + The +origin of the movement is defined by the alignment type.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\pos(<x>, +<y>) Defaults +to \move(<x>, <y>, <x>, <y>, 0, 0)</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\org(<x>, <y>) Moves +the default origin at (x,y). This is useful when moving subtitles in the +direction of rotation.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">WARNING: +\t, \move and \pos will ignore collusion detection.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\fade(<a1>, +<a2>, <a3>, <t1>, <t2>, <t3>, <t4>)</span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><a1> Alpha value +before <t1></span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><a2> Alpha value +between <t2> and <t3></span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><a3> Alpha value after +<t4></span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><t1>, <t4> +Animation beginning, ending time offset [ms]</span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><t1> - <t2> +Alpha value will be interpolated between <a1> and <a2></span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><t2> - <t3> +Alpha value will be set to <a2></span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><t3> - <t4> +Alpha value will be interpolated between <a2> and <a3></span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\fad(<t1>, +<t2>) <t1> the +time length of fading in</span></b></p> + +<p class="MsoNormal" style="margin-left:113.4pt;text-indent:-14.15pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""><t2> the time length +of fading out</span></b></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\clip(<x1>, <y1>, <x2>, <y2>)</span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + Clips +any drawing outside the rectangle defined by the parameters.</span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\clip([<scale>,] <drawing commands>)</span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + Clipping +against drawn shapes. </span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + <scale> +has the same meaning as in the case of \p<scale></span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><u><span style="font-size: 12pt; font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Drawings:</span></u></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\p<scale> <scale> +</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + + + Turns +on drawing mode and sets the magnification level of the coordinates at +the same +time. Scale is interpreted as two to the power of (<scale> minus +one). +For example {\p4} and the coordinate (8,16) will mean the same as {\p1} +and +(1,2). This feature can be useful for sub-pixel accuracy.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + If +0, drawing mode is turned off and the text is interpreted as usual.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">\pbo<y> + <y> +baseline offset. By default any drawings are positioned on the current +baseline. With this value it is possible to move them up or down by +<y> +pixels. (up: y<0, down: y>0)</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Drawing commands:</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">m + <x> +<y> + Moves +the cursor to <x>, <y></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">n + <x> +<y> + Moves +the cursor to <x>, <y> (unclosed shapes will be left open)</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">l + <x> +<y> + Draws +a line to <x>, <y></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">b <x1> <y1> +<x2> <y2> <x3> <y3></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + 3rd +degree bezier curve to point 3 using point 1 and 2 as the control points</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">s <x1> <y1> +<x2> <y2> <x3> <y3> .. <xN> <yN></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> + 3rd +degree uniform b-spline to point N, must contain at least 3 coordinates</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">p + <x> +<y> + extend +b-spline to <x>, <y></span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">c + + + close +b-spline</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Things you should know:</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Commands +must appear after {\p1+} and before {\p0}. (except +for \clip(..))</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Drawings +must always start with a move to command.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Drawings +must form a closed shape.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> All +unclosed shape will be closed with a straight line automatically.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Overlapping +shapes in the Dialogue line will be XOR-ed with each-other.</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:14.2pt;text-indent:-14.2pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> If +the same command follows another, it isn��t needed to write its identifier +letter again, only the coordinates.</span></b></p> + +<p class="MsoNormal" style="margin-left:14.2pt;text-indent:-14.2pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:14.2pt;text-indent:-14.2pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> The +coordinates are relative to the current cursor position (baseline) and the +alignment mode.</span></b></p> + +<p class="MsoNormal" style="margin-left:14.2pt;text-indent:-14.2pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:14.2pt;text-indent:-14.2pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Commands +p and c should only follow other b-spline commands.</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color="">Examples:</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Square: +m 0 0 l 100 0 100 100 0 100</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Rounded +square: m 0 0 s 100 0 100 100 0 100 c (c equals to ��p 0 0 100 +0 100 100�� in this case)</span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="margin-left:99.0pt;text-indent:-99.0pt"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> Circle +(almost): m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0 +(note that the 2nd ��b�� is optional here)</span></b></p> + +<p class="MsoNormal"><b><span style="font-family: "Arial", "sans-serif"; color: red; --darkreader-inline-color: #ff1a1a;" data-darkreader-inline-color=""> </span></b></p> + +<p class="MsoNormal" style="text-indent:.25pt"><b><span style="font-family:"Arial","sans-serif""> </span></b></p> + +<b><span style="font-size:10.0pt;font-family:"Arial","sans-serif""><br style="page-break-before:always" clear="all"> +</span></b> + +<p class="MsoNormal"><b><u><span style="font-size:14.0pt;font-family:"Arial","sans-serif"">Appendix +B: embedded font/picture encoding</span></u></b></p> + +<p class="MsoNormal"><span style="font-size:12.0pt;font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">SSA��s font +and picture file embeddeding is a form of UUEncoding.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">It takes a +binary file, three bytes at a time, and converts the 24bits of those bytes into +four 6-bit numbers. 33 is added to each of these four numbers, and the +corresponding ascii character for each number is written into the script file.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">The offset of +33 means that lower-case characters cannot appear in the encoded output, and +this is why the ��filename�� lines are always lower case.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">Each line of +an encoded file is 80 characters long, except the last one, which may be +shorter.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">If the length +of the file being encoded is not an exact multiple of 3, then for odd-number +filelengths, the last byte is multiplied by hexadecimal 100, and the most +significant 12 bits are converted to two characters as above. For even-number +filelengths, the last two bytes are multiplied by hexadecimal 10000, and the +most significant 18 bits are converted to three characters as above. </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif"">There is no +terminating code for the embedded files. If a new [section] starts in the +script, or if another filename line is found, or the end of the script file is +reached then the file is considered complete.</span></p> + +<p class="MsoNormal"><span style="font-family:"Arial","sans-serif""> </span></p> + +</div> + + + + +</body><grammarly-desktop-integration data-grammarly-shadow-root="true"></grammarly-desktop-integration></html> \ No newline at end of file diff --git a/utils/vvs/retime.vvl b/utils/vvs/retime.vvl index f97cabe4d552c1f6003ed1c5f1f485d0dd9b061a..8ad099050d83b60dfc5b617049906f50fbe55850 100644 --- a/utils/vvs/retime.vvl +++ b/utils/vvs/retime.vvl @@ -8,7 +8,7 @@ option "after" { type = "number", default = 300 } -- You can add a field to any ASS item. It will be accessible by doing a -- `line.aux.number` here. Note that name collision will raise an error... -data "line:number" { default = 0 } +data "line:number" { type = "number", default = 0 } -- To declare a job we must give it a name. The function to use must follow -- some rules. It must only have one argument, the item on which it must be @@ -16,7 +16,7 @@ data "line:number" { default = 0 } -- of things, i.e. one ASS item or a table of the same ASS item. To be sure of -- what is returning (a check will be done), you must specify the return type of -- the function. -job "prelines" { +job "preline" { line = function(line) -- Single argument, line/lines/syllabe/syllabes local pre_line = line:copy() pre_line.start = line.start - before @@ -29,7 +29,7 @@ job "prelines" { end } -job "postlines" { +job "postline" { line = function(line) local post_line = line:copy() post_line.start = line.fini diff --git a/utils/vvs/test_data.vvs b/utils/vvs/test_data.vvs index 01553af81813ddbb4092976593c7f3fe24ebe670..17c427db7423d9f81fa69dbea2306e91a8f364cc 100644 --- a/utils/vvs/test_data.vvs +++ b/utils/vvs/test_data.vvs @@ -1,3 +1,6 @@ -- vim: ft=lua -data "line:number" { type = "number", value = 0 } +data "line:count" { type = "number", value = 0 } + +-- Invalid! +-- data "syllabes:toto"