pax_global_header00006660000000000000000000000064146442531110014513gustar00rootroot0000000000000052 comment=b29838961b7a1912838319b9de9c2f577d72e041 subtile-0.1.9/000077500000000000000000000000001464425311100131715ustar00rootroot00000000000000subtile-0.1.9/.github/000077500000000000000000000000001464425311100145315ustar00rootroot00000000000000subtile-0.1.9/.github/workflows/000077500000000000000000000000001464425311100165665ustar00rootroot00000000000000subtile-0.1.9/.github/workflows/code_check.yml000066400000000000000000000051251464425311100213630ustar00rootroot00000000000000name: CI - Code Checks & Tests run-name: Checks Rust code on ${{ github.event_name }} on: push: paths: - ".github/workflows/build.yml" - "Cargo.*" - "src/**" pull_request: paths: - ".github/workflows/build.yml" - "Cargo.*" - "src/**" env: CARGO_TERM_COLOR: always jobs: ci_code_checks_ans_tests: runs-on: ubuntu-latest strategy: fail-fast: false name: Code Checks and Tests steps: - name: "Install rust-toolchain stable" uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - name: "Show environment" run: | rustc -vV cargo -vV - name: "Checkout" uses: actions/checkout@v4 - name: Install cargo-readme run: cargo install cargo-readme - name: Check if readme is up to date id: cargo_readme if: $${{ always() }} run: diff README.md <(cargo readme) - name: "Cargo clippy" id: cargo_clippy if: $${{ always() }} run: | cargo clippy --profile=test - name: "Cargo test" id: cargo_test if: $${{ always() }} run: | cargo test - name: "Cargo formatting" id: cargo_fmt if: $${{ always() }} run: | cargo fmt --all -- --check - name: "Cargo doc" id: cargo_doc env: RUSTDOCFLAGS: "-D warnings" if: $${{ always() }} run: | cargo doc - name: "Some checks failed" if: ${{ failure() }} run: | echo "### :x: Checks Failed!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "|Job|Status|" >> $GITHUB_STEP_SUMMARY echo "|---|------|" >> $GITHUB_STEP_SUMMARY echo "|Cargo readme|${{ steps.cargo_readme.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|Cargo clippy|${{ steps.cargo_clippy.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|Cargo test|${{ steps.cargo_test.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|Cargo fmt|${{ steps.cargo_fmt.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "|Cargo doc|${{ steps.cargo_doc.outcome }}|" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Please check the failed jobs and fix where needed." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY exit 1 - name: "All checks passed" if: ${{ success() }} run: | echo "### :white_check_mark: Checks Passed!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY subtile-0.1.9/.gitignore000066400000000000000000000000161464425311100151560ustar00rootroot00000000000000/target *.origsubtile-0.1.9/Cargo.lock000066400000000000000000000300621464425311100150770ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytemuck" version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "colorchoice" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "fdeflate" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "image" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", "num-traits", "png", ] [[package]] name = "is_terminal_polyfill" version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "png" version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ "bitflags", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", "syn", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "subtile" version = "0.1.9" dependencies = [ "cast", "env_logger", "glob", "image", "log", "nom", "once_cell", "profiling", "regex", "thiserror", ] [[package]] name = "syn" version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" subtile-0.1.9/Cargo.toml000066400000000000000000000035631464425311100151300ustar00rootroot00000000000000[package] name = "subtile" version = "0.1.9" edition = "2021" description = "A crate of utils to operate traitements on subtitles" repository = "https://github.com/gwen-lg/subtile" authors = ["Eric Kidd ", "Gwen Lg "] keywords = ["subtitle", "library", "parse", "write", "vobsub"] categories = [ "command-line-utilities", "encoding", "multimedia::encoding", "parser-implementations", ] license = "LGPL-3.0-or-later" [badges] maintenance = { status = "actively-developed" } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lints.rust] missing_docs = "deny" unused_imports = "deny" [lints.clippy] cargo = { priority = -1, level = "warn" } complexity = { priority = -1, level = "deny" } correctness = { priority = -1, level = "deny" } # nursery = { priority = -1, level = "deny" } # pedantic = { priority = -1, level = "warn" } perf = { priority = -1, level = "deny" } # restriction = { priority = -1, level = "deny" } style = { priority = -1, level = "deny" } # suspicious = { priority = -1, level = "deny" } bind_instead_of_map = "deny" borrowed_box = "deny" cast_lossless = "deny" clone_on_copy = "deny" derive_partial_eq_without_eq = "deny" doc_markdown = "deny" extra_unused_lifetimes = "deny" if_not_else = "deny" match_same_arms = "deny" missing_const_for_fn = "deny" missing_errors_doc = "deny" missing_fields_in_debug = "deny" missing_panics_doc = "deny" must_use_candidate = "deny" or_fun_call = "deny" trivially_copy_pass_by_ref = "deny" uninlined_format_args = "deny" use_self = "deny" unreadable_literal = "deny" useless_conversion = "deny" [dependencies] cast = "0.3" image = { version = "0.25", default-features = false, features = ["png"] } log = "0.4" nom = "7.1" once_cell = "1.19" profiling = "1.0" regex = "1.10" thiserror = "1.0" [dev-dependencies] env_logger = "0.11" glob = "0.3" subtile-0.1.9/LICENSE.txt000066400000000000000000000167441464425311100150300ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. subtile-0.1.9/README.md000066400000000000000000000021301464425311100144440ustar00rootroot00000000000000# subtile ## Current version: 0.1.9 ![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg) [![crates.io](https://img.shields.io/crates/v/subtile.svg)](https://crates.io/crates/subtile) [![docs.rs](https://docs.rs/subtile/badge.svg)](https://docs.rs/subtile/) [![dependency status](https://deps.rs/crate/subtile/0.1.9/status.svg)](https://deps.rs/crate/subtile/0.1.9) `subtile` is a Rust library which aims to propose a set of operations for working on subtitles. Example: parsing from and export in different formats, transform, adjust, correct, ... ## Project ### start The project started with the fork of [vobsub](https://crates.io/crates/vobsub) crate which no longer seems to be maintained. Beyond the simple recovery, I want to take the opportunity to improve the code and extend the provided features. ### Name `Subtile` is a french word than fit well as contraction of Subtitles Utils. ### Contributing Your feedback and contributions are welcome! Please see [Subtile](https://github.com/gwen-lg/subtile) on GitHub for details. ## License : LGPL-3.0-or-later subtile-0.1.9/README.tpl000066400000000000000000000006121464425311100146460ustar00rootroot00000000000000# {{crate}} ## Current version: {{version}} {{badges}} [![crates.io](https://img.shields.io/crates/v/{{crate}}.svg)](https://crates.io/crates/{{crate}}) [![docs.rs](https://docs.rs/{{crate}}/badge.svg)](https://docs.rs/{{crate}}/) [![dependency status](https://deps.rs/crate/{{crate}}/{{version}}/status.svg)](https://deps.rs/crate/{{crate}}/{{version}}) {{readme}} ## License : {{license}} subtile-0.1.9/fixtures/000077500000000000000000000000001464425311100150425ustar00rootroot00000000000000subtile-0.1.9/fixtures/example.idx000066400000000000000000000021271464425311100172050ustar00rootroot00000000000000# VobSub index file, v7 (do not modify this line!) # Created by BDSup2Sub++ 1.0.2 # Frame size size: 1920x1080 # Origin - upper-left corner org: 0, 0 # Scaling scale: 100%, 100% # Alpha blending alpha: 100% # Smoothing smooth: OFF # Fade in/out in milliseconds fadein/out: 0, 0 # Force subtitle placement relative to (org.x, org.y) align: OFF at LEFT TOP # For correcting non-progressive desync. (in millisecs or hh:mm:ss:ms) time offset: 0 # ON: displays only forced subtitles, OFF: shows everything forced subs: OFF # The palette of the generated file palette: 000000, f0f0f0, cccccc, 999999, 3333fa, 1111bb, fa3333, bb1111, 33fa33, 11bb11, fafa33, bbbb11, fa33fa, bb11bb, 33fafa, 11bbbb # Custom colors (transp idxs and the four colors) custom colors: OFF, tridx: 1000, colors: 000000, 444444, 888888, cccccc # Language index in use langidx: 0 # German id: de, index: 0 # Decomment next line to activate alternative name in DirectVobSub / Windows Media Player 6.x # alt: German # Vob/Cell ID: 1, 1 (PTS: 0) timestamp: 00:00:49:466, filepos: 000000000 timestamp: 00:00:52:636, filepos: 000001000 subtile-0.1.9/fixtures/example.sub000066400000000000000000000300001464425311100172010ustar00rootroot00000000000000КDФ‚Љ‰УјНь€!мз ˆ jB0H# Т0Œ# 0`# €_ €БŸ<БŸБ_ №€БV№”ЖoKa№ЬЖVЫ№РБ‘3‹ЫЫЫa0Жa0PЖa0ШЖ–№№БQ0МБ‘3‹ЫЫЫa0Жa0PЖa0ШЖ№шБQ0МБ‘3‹+ЫЫa0Жa0T/ ‹a“і!k‚АМБ‘3‹ 0#,БV3 a3<#8' aGaжѓ‚0`4#РБ‘3‹Тp”Б‘1С:Ё1‹a:Џ aaZ +УvЁ0`Ж0ёжђ:Ё2Bzc2A:Ё0XБ‘3‹ЫЫК=ё gcVАXЖgcQ1KaKaГV№XЖ8БпЖ=a1‹v)ёQ1K`AoРБ‘3‹ЫЫЫ`QјА]№PЖ‘<Жa4Ж–№PЖ<Б–ќЖБK=Ы`Qk€БSЫЫ‹aжaпK%aЖ№LЖ!Ёzo‹aKbaж№LЖa4БкaжќБжс1Q8ЖЁі№@АЫЫKaV3bЖo‹a–/oKaKa–+aV‹a‘0@vc1–јБ“qQ4Б“vѓРАЫЫKaБKaQq‘ёaVKaVЖaKaVБV‹a‘0@Б‘v#іoKСqQ4v#ёVѓ€БRЫЫaQr1VЖa0DЖa2aVaKaQq‹a‹a‘0@Б‘qV$БŸKa1Ыvѓ€БSЫЫa2ТБVЖa0DЖa2Kava4Жa1Ыa‹a‘0@v,v БQАq4БSЫЫ a1ЫaKa$Жava4Жa1Ыa‹a‘3ЫaqQ3  БQАq4Б‘3‹ЫЫ a1ЫaKa$Жava4Жa1Ыa‹a‘3ЫaqQ3  БQБP^4Б‘3‹ЫЫѓєБVЖa0DЖ$vava4Жa1Ыa‹a‘3‹aБVђЫ БQБVГ@Б‘3‹ЫЫa_,Бa1ЫaKaVђ aVaKaЖa0HЖ4БбёŸ,БV БQБg8Б‘3‹ЫЫKђKa1ЫaKa‘1‹aVaKaЖa0HЖ(і‹o ё‘ёQ2 KђOѓРБ‘3‹Ы#Ы‡a–С1‘qVЖa0DЖoЯa‘іa4Жa1Ыa‹a“Ж!aМvС6a4БQ2 ‡a–ёA1–ќБ‘3‹ЫЫK‹bVК!a1ЫaKbšv!atЖa4Жa1Ыa‹`qaqKbVЖ%atБQ2 ‹bVі%a8Б_€Б‘3‹ЫЫKЖбёVЖa0DЖЃбИЖa4Жa1Ыa‹`eaђ `IaИБQ2 ЖИБQ4Б‘3‹ЫЫKcQєЖЖa0DЖkb–ЫaKaЖa0HЖ',5aё  БQ1A3жБQ4BѓТёБАX/ 1ђ№HБ‚ј/ /РS+$+ qТДТ0H#B3ВB2Т0T#C2Т1B2Т0X№d;000?0€+БBp”+BА˜+‚Аа0BpФБ‘3‹ЫЫЫa0Жa0PЖa0ШЖ№јБQ0МБ‘3‹ЫЫЫa0Жa0PЖa0ШЖж№ьБQ0МБ‘3‹Т№ŒБ‘0|БV a0ШЖ/bж№ф/ РБ‘3‹'ЫЫa0Жa0X+ ‹aCaж№ш' РБ‘3‹ ‹$; БV;‹a?(##CАdЖ(a–ђУГД+ѓА`Б‘3‹Ы№Б‘1cёKkbпЫaЖ)a1‹aјЖob–№\Ж4Жoі1o Нkbё_і1o@Б‘3‹ЫЫЖŸЫЃжАTЖЃжё aKasж№TЖЁ2BrOссZAzя 3ЁZ№dЖaГ a–ђzЁ3:Ё6a1A~сrСzЁqKaЁZa6Ё2qС6ЁqBsBq:Ё2BzкќЁ^4Ё^БQ:Ё1rBr@K('3ЁZѓOЁZ8Б‘1Kcжё Ж5k‹К=oБQ1cVqQ1Ocж№`іVёKgcVАXЖ<Жa1OcжђcVva<і‘1ЯcжќЖgcVјЖ5a6k5oЖa<Ж=oo$ёV‹cжБbVcжђKcжё v5o‡aQ1ЧaVёЧaQ1Q2 ‹Ж5k і5g8Б‘<ЖжјБV№PБQ<№QkБQ<Ж‘<№QkOŸБV_ apH/Я`MoЖ–O`Uo№QoK`Yav5a6–ќЖa4№MoKaVђ `QёK=жёK`Mo‹–ё‹Б‘1Q2 ‹`EёK`AaQ3€Б‘8БкaпK%aЖoЫ‹aжaпЫ‹aЖ%‹aКDФ‚Љ‰УјНь жaпЫb6ќВVaп‹a‘pTЖaњќЖЁv%a4Бж#Ё–јЖaКav!Ёza~baжјЖa4Б–a–єБ‘1‹aVašЁ–ќБжяaіoЫЁЖєВVЁ–oВёGaQqQ2 ‹%azoЫaЖ%8Б‘4Б–/a‘qвё‘0HБQ4ЖcqŸ‹KaV/KaV7№DБ–3aQ8Бвё‘0DЖ aV7aVaV/aЖc6a4v3a6bЖb6bvbЖa4ЖaЖ/aёVёOaБ“vo‹3/K3aV+a‘qŸВQ<БV БQ8Бвё‘4Б’і8Б‘4vaБKaQq‘ќЖ‹K#ёVєБQ4БVё‘4БR1Oo>БQ8Б‘ќЖa0@Б– |БVЖaqVБ–OЖaБVБVaVБV‹a'aVБVKaaVЯсДЖoБ‘Жb1OKa|aq сДvБVЫaQБVјБVјЖ БQ8Б‘ќЖGaQё a‘3€Б‘Б‘r1VБQ0HБQБVq_KЖaq_€KЁQ8БVБV a2 `Aav`GoaQqЫaVРGЁVaV$/aQq‹a‹aЫaQq‹aKa!ујБ‘8ЖaБPGaєБVВqV 3a1ЫGaQ4vgK БQ8БVБQБVБ‘3€Б‘БV,+ БQ0HБQАqa2K0L9‹o БV aV Ж`moa$БV5ava3va1Ыa‹aЫaЖa4Жa4Ж5сqoOaЖБQ2 9Ёq ,+ БQ4БVЖK БQ8БQ2 $v8Б‘БQ0PБQ2 ‹БQБQ2‹0DіQ8БVђ a0@Ж Вс–`moa$Жa<ЖVaЫaЖa8Жa<Жa1ЫaKaЫ`Eё‹aV`q$ЖQ8БQ0PБQ2 ‡aQ6ooaQtБQ2 ‹ БQБQ2‹8Б‘БQ0PБQ2 ‹ЁБQБQ2‹Ё0@АAсQ8БVђ a0DБ–ђC6avЁ6a2KaKcоava0LЖa1Ыa‹aЫaЖa4Жa1OЃжБKaVKaP^,ў9aqQ0PБQ2 ‹oa_a8БQ2 ‹ БQБQ2‹8Б‘БVђТ4БQ2 ‹`sђ‡aQБVГЫЂёQ8БVђ a0DЖ(+aVaPkaQ2KaЖЂіav,KaЖa8Жa<Жa1ЫaKa+тVќЖoatЖ`s(+тVo,‹ БQ<Б[agaQxБQ2 ‹ БQБVђ‡aQ3€Б‘БŸ,Б БQ0HБQБgKa_$Б‘ЖsЫ;aQ8БVђ a0HЖKaVaVvo Жavcvavo(ЖoaЖa8Жa<Жa1ЫaKa7‚va<ЖgЫ`gK'‚vo(Жa1Q2 vqQЖqVЫЖ‹ БQБŸ$Б‘3€Б‘4БŸ$Б‘1Q2 ‹K$ЖKKёЫa‘4Б‘2KaѓЫaq‹o БVЫa–ѓЯaЖo$ЖavoЖavaБOaVaVђ aVіa1Ыa‹aЫaЖa4ЖavoaБ–a1 aVђOo‹БVё6o ёV БQ1 aK-БVё‹ БQ4БŸЖ8Б‘8v БQ0HБQ8va11–єБQ8voЯ!‡aб11–ѓЫo%јБVђ a0PЖ!a1v!a4Бжё6oЯaжaVё6!Ka<aVaЖa8Жa<Жa1ЫaKaoіa1 b–Жoa–єБQ1‹aVёOaVa‘<a‘qQ2 v%‹%Б–ќі‹ БQ8vё!РБ‘8Ж%і% БQ0HБQ8Ж%obVK!kbб8ђŸbV<Ж!oc_‹o БVObкК5O)kЂR2Vz)av!ЋcVєђVК!ava1Ыa‹a–aЖa4Жa4ВVі!aqKbQqЫbVі%a4БQ1Ы%obbі% БQ1K!a<Вv!kbб8БQ2 ‹bЖ-ЫєБ‘1 `IaёQ2 ‹ЖДБQ1 КDФ‚Љ‰УјНю cзЖА@Ж—aVєБVђ a0\іR|Ж‹cкa№IЁVќ№MaЖa1Ыa)oaЖa4Жa4іжВ(АQaДБQ2 `Ma|АMaёQ2 Ббq v=g‹ БQ1 cзЫБ‘1A3‘єБQ2 ‹c–Ыі-ёQ1A69aАPЖ5aQЖ ЖAvRБС69aёA6)kacaVќcV'aЖa<і!oaЖa4Жa<9aђarС3–Ы(ѓ–5aєБQ2 va1Ka2O-o‹ БQ1ObпЫBђS+$+ТВW+rWwBВB№pѓ@[$Б{|/ /2ј/ВУsРW+0r€S+$+ /+0|+$+qАТ3ВAѓ№X0?,,3ѓ№t;0€OC<7B1Уёђƒђ2Т24#,##$0@+УђС№@?ВAѓђСђС№@3ёѓђУ8@—џ№U93Х ’џ%—џОјџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџsubtile-0.1.9/fixtures/tiny-split.idx000066400000000000000000000034671464425311100176760ustar00rootroot00000000000000# VobSub index file, v7 (do not modify this line!) # # To repair desynchronization, you can insert gaps this way: # (it usually happens after vob id changes) # # delay: [sign]hh:mm:ss:ms # # Where: # [sign]: +, - (optional) # hh: hours (0 <= hh) # mm/ss: minutes/seconds (0 <= mm/ss <= 59) # ms: milliseconds (0 <= ms <= 999) # # Note: You can't position a sub before the previous with a negative value. # # You can also modify timestamps or delete a few subs you don't like. # Just make sure they stay in increasing order. # Settings # Original frame size size: 718x480 # Origin, relative to the upper-left corner, can be overloaded by aligment org: 0, 0 # Image scaling (hor,ver), origin is at the upper-left corner or at the alignment coord (x, y) scale: 100%, 100% # Alpha blending alpha: 100% # Smoothing for very blocky images (use OLD for no filtering) smooth: OFF # In milliseconds fadein/out: 50, 50 # Force subtitle placement relative to (org.x, org.y) align: OFF at LEFT TOP # For correcting non-progressive desync. (in milliseconds or hh:mm:ss:ms) # Note: Not effective in DirectVobSub, use 'delay: ... ' instead. time offset: 0 # ON: displays only forced subtitles, OFF: shows everything forced subs: OFF # The original palette of the DVD palette: 000000, ffffff, 000000, 000000, 828282, 828282, 828282, ffffff, 828282, bababa, 828282, 828282, 828282, 828282, 828282, 828282 # Custom colors (transp idxs and the four colors) custom colors: OFF, tridx: 0000, colors: 000000, 000000, 000000, 000000 # Language index in use langidx: 0 # English id: en, index: 0 # Decomment next line to activate alternative name in DirectVobSub / Windows Media Player 6.x # alt: English # Vob/Cell ID: 1, 1 (PTS: 0) timestamp: 00:00:01:000, filepos: 000000000subtile-0.1.9/fixtures/tiny-split.sub000066400000000000000000000003171464425311100176720ustar00rootroot00000000000000КDФ‚Љ‰УјНƒ€!П! Šl4444444444444444444444‚hŠЈŠЈŠЈ-Ј-ЈŠшŠu~Р‡q4444444444444444444444444‚hŠЈŠЈ-ЈЮк€ŽзЈŠзшчР4444„2џ№џџџџџџџџџКDФ‚Љ‰УјН€!П! lба9џЎ„џџџsubtile-0.1.9/fixtures/tiny.idx000066400000000000000000000034671464425311100165450ustar00rootroot00000000000000# VobSub index file, v7 (do not modify this line!) # # To repair desynchronization, you can insert gaps this way: # (it usually happens after vob id changes) # # delay: [sign]hh:mm:ss:ms # # Where: # [sign]: +, - (optional) # hh: hours (0 <= hh) # mm/ss: minutes/seconds (0 <= mm/ss <= 59) # ms: milliseconds (0 <= ms <= 999) # # Note: You can't position a sub before the previous with a negative value. # # You can also modify timestamps or delete a few subs you don't like. # Just make sure they stay in increasing order. # Settings # Original frame size size: 718x480 # Origin, relative to the upper-left corner, can be overloaded by aligment org: 0, 0 # Image scaling (hor,ver), origin is at the upper-left corner or at the alignment coord (x, y) scale: 100%, 100% # Alpha blending alpha: 100% # Smoothing for very blocky images (use OLD for no filtering) smooth: OFF # In milliseconds fadein/out: 50, 50 # Force subtitle placement relative to (org.x, org.y) align: OFF at LEFT TOP # For correcting non-progressive desync. (in milliseconds or hh:mm:ss:ms) # Note: Not effective in DirectVobSub, use 'delay: ... ' instead. time offset: 0 # ON: displays only forced subtitles, OFF: shows everything forced subs: OFF # The original palette of the DVD palette: 000000, ffffff, 000000, 000000, 828282, 828282, 828282, ffffff, 828282, bababa, 828282, 828282, 828282, 828282, 828282, 828282 # Custom colors (transp idxs and the four colors) custom colors: OFF, tridx: 0000, colors: 000000, 000000, 000000, 000000 # Language index in use langidx: 0 # English id: en, index: 0 # Decomment next line to activate alternative name in DirectVobSub / Windows Media Player 6.x # alt: English # Vob/Cell ID: 1, 1 (PTS: 0) timestamp: 00:00:01:000, filepos: 000000000subtile-0.1.9/fixtures/tiny.srt000066400000000000000000000000521464425311100165540ustar00rootroot00000000000000яЛП1 00:00:01,000 --> 00:00:03,000 , subtile-0.1.9/fixtures/tiny.sub000066400000000000000000000040001464425311100165320ustar00rootroot00000000000000КDФ‚Љ‰УјН“€!П! Šl4444444444444444444444‚hŠЈŠЈŠЈ-Ј-ЈŠшŠu~Р‡q4444444444444444444444444‚hŠЈŠЈ-ЈЮк€ŽзЈŠзшчР4444„2џ№lба9џЎ„џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџsubtile-0.1.9/src/000077500000000000000000000000001464425311100137605ustar00rootroot00000000000000subtile-0.1.9/src/content/000077500000000000000000000000001464425311100154325ustar00rootroot00000000000000subtile-0.1.9/src/content/area.rs000066400000000000000000000034731464425311100167170ustar00rootroot00000000000000use super::{ContentError, Size}; /// Location at which to display the subtitle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AreaValues { /// min `x` coordinate value pub x1: u16, /// min `y` coordinate value pub y1: u16, /// max `x` coordinate value pub x2: u16, /// max `y` coordinate value pub y2: u16, } /// Location at which to display the subtitle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Area(AreaValues); impl Area { /// The leftmost edge of the subtitle. #[must_use] pub const fn left(&self) -> u16 { self.0.x1 } /// The rightmost edge of the subtitle. #[must_use] pub const fn top(&self) -> u16 { self.0.y1 } /// The width of the subtitle. #[must_use] pub const fn width(&self) -> u16 { self.0.x2 + 1 - self.0.x1 } /// The height of the subtitle. #[must_use] pub const fn height(&self) -> u16 { self.0.y2 + 1 - self.0.y1 } /// The size of the subtitle. #[must_use] pub fn size(&self) -> Size { Size { w: cast::usize(self.width()), h: cast::usize(self.height()), } } } impl TryFrom for Area { type Error = ContentError; fn try_from(coords_value: AreaValues) -> Result { // Check for weird bounding boxes. Ideally we // would do this while parsing, but I can't // figure out how to get nom to do what I want. // Later on, we assume that all bounding boxes // have non-negative width and height and we'll // crash if they don't. if coords_value.x2 <= coords_value.x1 || coords_value.y2 <= coords_value.y1 { Err(ContentError::InvalidAreaBounding) } else { Ok(Self(coords_value)) } } } subtile-0.1.9/src/content/mod.rs000066400000000000000000000006451464425311100165640ustar00rootroot00000000000000//! Module for subtitle content utils mod area; mod size; pub use area::{Area, AreaValues}; pub use size::Size; use thiserror::Error; /// Error for content #[derive(Debug, Error)] pub enum ContentError { /// Indicate an invalid bounding box Area /// Example: If at least one coordinate value of second point are inferior of first point. #[error("Invalid bounding box for Area")] InvalidAreaBounding, } subtile-0.1.9/src/content/size.rs000066400000000000000000000002341464425311100167510ustar00rootroot00000000000000/// The dimensions of an image. #[derive(Debug)] pub struct Size { /// Width in pixels. pub w: usize, /// Height in pixels. pub h: usize, } subtile-0.1.9/src/errors.rs000066400000000000000000000010121464425311100156340ustar00rootroot00000000000000//! Custom error types. use thiserror::Error; /// A type representing errors that are specific to `subtile`. Note that we may /// normally return `Error`, not `SubError`, which allows to return other /// kinds of errors from third-party libraries. #[derive(Debug, Error)] pub enum SubError { /// Error with `VobSub` #[error("Error with VobSub")] VobSub(#[from] crate::vobsub::VobSubError), /// Error during image dump #[error("Dump images failed")] ImageDump(#[from] crate::image::DumpError), } subtile-0.1.9/src/image/000077500000000000000000000000001464425311100150425ustar00rootroot00000000000000subtile-0.1.9/src/image/mod.rs000066400000000000000000000001321464425311100161630ustar00rootroot00000000000000//! Module for `Image` manipulation. mod utils; pub use utils::{dump_images, DumpError}; subtile-0.1.9/src/image/utils.rs000066400000000000000000000041641464425311100165550ustar00rootroot00000000000000use image::{EncodableLayout, Pixel, PixelWithColorType}; use std::{ fs::create_dir_all, io, ops::Deref, path::{Path, PathBuf}, }; use thiserror::Error; use crate::SubError; /// Handle Error for image dump. #[derive(Error, Debug)] pub enum DumpError { /// Error with path creation #[error("Could not create path for dump images '{}'", path.display())] Folder { /// Path of the folder path: PathBuf, /// Error source source: io::Error, }, /// Error during file dump #[error("Could not write image dump file '{}'", filename.display())] DumpImage { /// Path of the file write failed filename: PathBuf, /// Error source source: image::ImageError, }, } /// Dump some images in a folder specified by the path. #[profiling::function] pub fn dump_images<'a, Img, P, Container>(path: &str, images: Img) -> Result<(), SubError> where P: Pixel + PixelWithColorType + 'a, [P::Subpixel]: EncodableLayout, Container: Deref + 'a, Img: IntoIterator>, { let folder_path = PathBuf::from(path); // create path if not exist if !folder_path.is_dir() { create_dir_all(folder_path.as_path()).map_err(|source| DumpError::Folder { path: folder_path.clone(), source, })?; } images .into_iter() .enumerate() .try_for_each(move |(i, img)| { let mut filepath = folder_path.clone(); filepath.push(format!("{i:06}.png")); dump_image(&filepath, img).map_err(|source| DumpError::DumpImage { filename: filepath, source, }) })?; Ok(()) } /// Dump one image #[profiling::function] fn dump_image( filename: P, image: &image::ImageBuffer, // image::Luma, Vec ) -> Result<(), image::ImageError> where P: AsRef, Pix: Pixel + PixelWithColorType, [Pix::Subpixel]: EncodableLayout, Container: Deref, { image.save(filename) } subtile-0.1.9/src/lib.rs000066400000000000000000000016211464425311100150740ustar00rootroot00000000000000//! `subtile` is a Rust library which aims to propose a set of operations //! for working on subtitles. Example: parsing from and export in different formats, //! transform, adjust, correct, ... //! //! # Project //! ## start //! The project started with the fork of [vobsub](https://crates.io/crates/vobsub) //! crate which no longer seems to be maintained. //! Beyond the simple recovery, I want to take the opportunity to improve the code //! and extend the provided features. //! //! ## Name //! `Subtile` is a french word than fit well as contraction of Subtitles Utils. //! //! ## Contributing //! //! Your feedback and contributions are welcome! Please see //! [Subtile](https://github.com/gwen-lg/subtile) on GitHub for details. // For error-chain. #![recursion_limit = "1024"] pub mod content; mod errors; pub mod image; pub mod srt; pub mod time; mod util; pub mod vobsub; pub use errors::SubError; subtile-0.1.9/src/srt.rs000066400000000000000000000014321464425311100151360ustar00rootroot00000000000000//! SubRip/Srt functionality use std::io; use crate::time::TimeSpan; /// Write subtitles in srt format /// # Errors /// /// Will return `Err` if write in `writer` return an `Err`. pub fn write_srt( writer: &mut impl io::Write, subtitles: &[(TimeSpan, String)], ) -> Result<(), io::Error> { subtitles .iter() .enumerate() .try_for_each(write_srt_line(writer))?; Ok(()) } /// Write an subtitle line in Srt format fn write_srt_line( writer: &mut impl io::Write, ) -> impl FnMut((usize, &(TimeSpan, String))) -> Result<(), io::Error> + '_ { |(idx, (time_span, text))| { let line_num = idx + 1; let start = time_span.start; let end = time_span.end; writeln!(writer, "{line_num}\n{start} --> {end}\n{text}") } } subtile-0.1.9/src/time/000077500000000000000000000000001464425311100147165ustar00rootroot00000000000000subtile-0.1.9/src/time/mod.rs000066400000000000000000000001711464425311100160420ustar00rootroot00000000000000//! Subtitle Time management mod time_point; mod time_span; pub use time_point::TimePoint; pub use time_span::TimeSpan; subtile-0.1.9/src/time/time_point.rs000066400000000000000000000052561464425311100174430ustar00rootroot00000000000000use core::fmt; use std::ops::Neg; /// Define a time in milliseconds #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct TimePoint(i64); impl TimePoint { /// Create a `TimePoint` from miliseconds #[must_use] pub const fn from_msecs(time: i64) -> Self { Self(time) } /// Create a `TimePoint` from seconds /// /// # Panics /// /// Will panics if the `seconds` value fill as parameter is to big to be store as /// millisecond in a i64. #[must_use] pub fn from_secs(seconds: f64) -> Self { let msecs = cast::i64(seconds * 1000.0).unwrap(); Self(msecs) } /// Convert to seconds #[must_use] pub fn to_secs(self) -> f64 { self.0 as f64 / 1000. } const fn msecs(self) -> i64 { self.0 } const fn secs(self) -> i64 { self.0 / 1000 } const fn mins(self) -> i64 { self.0 / (60 * 1000) } const fn hours(self) -> i64 { self.0 / (60 * 60 * 1000) } const fn mins_comp(self) -> i64 { self.mins() % 60 } const fn secs_comp(self) -> i64 { self.secs() % 60 } const fn msecs_comp(self) -> i64 { self.msecs() % 1000 } } impl Neg for TimePoint { type Output = Self; fn neg(self) -> Self { Self(-self.0) } } impl fmt::Display for TimePoint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let t = if self.0 < 0 { -*self } else { *self }; write!( f, "{}{:02}:{:02}:{:02},{:03}", if self.0 < 0 { "-" } else { "" }, t.hours(), t.mins_comp(), t.secs_comp(), t.msecs_comp() ) } } #[cfg(test)] mod tests { use super::*; #[test] fn time_point_creation() { assert_eq!(TimePoint::from_msecs(6751), TimePoint(6751)); assert_eq!(TimePoint::from_msecs(142), TimePoint::from_secs(0.142)); } #[test] fn time_point_creation_with_too_mutch_decimals() { assert_eq!(TimePoint::from_msecs(265), TimePoint::from_secs(0.265_579)); assert_eq!(TimePoint(142), TimePoint::from_secs(0.142_75)); } #[test] fn time_point_msecs() { const TIME: i64 = 62487; assert_eq!(TimePoint::from_msecs(TIME).msecs(), TIME); } #[test] fn time_point_secs() { const TIME: f64 = 624.87; assert_eq!(TimePoint::from_secs(TIME).secs(), 624); } #[test] fn to_big_seconds() { const TIME: f64 = 9_223_372_036_854_775.808; // i64::MAX + 1 as f64 / 1000 let result = std::panic::catch_unwind(|| TimePoint::from_secs(TIME)); assert!(result.is_err()); } } subtile-0.1.9/src/time/time_span.rs000066400000000000000000000037051464425311100172500ustar00rootroot00000000000000use super::TimePoint; use core::fmt::{self, Debug}; /// Define a time span with a start time and an end time. #[derive(Clone, Copy, PartialEq, Eq)] pub struct TimeSpan { /// Start time of the span pub start: TimePoint, /// End time of the span pub end: TimePoint, } impl TimeSpan { /// Create a new `TimeSpan` from a start and an end. #[must_use] pub const fn new(start: TimePoint, end: TimePoint) -> Self { Self { start, end } } } impl Debug for TimeSpan { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} --> {}", self.start, self.end) } } #[cfg(test)] mod tests { use super::*; #[test] fn time_span_creation() { assert_eq!( TimeSpan::new(TimePoint::from_msecs(0), TimePoint::from_secs(1.34)), TimeSpan { start: TimePoint::from_msecs(0), end: TimePoint::from_secs(1.34) } ); } #[test] fn time_span_equality() { let time_span_0_1 = TimeSpan::new(TimePoint::from_msecs(0), TimePoint::from_secs(1.34)); let time_span_1_2 = TimeSpan::new(TimePoint::from_msecs(1245), TimePoint::from_secs(2.34)); assert_eq!( time_span_0_1, TimeSpan::new(TimePoint::from_msecs(0), TimePoint::from_secs(1.34)) ); assert_eq!( time_span_1_2, TimeSpan { start: TimePoint::from_msecs(1245), end: TimePoint::from_secs(2.34) } ); } #[test] fn time_span_nequality() { let time_span_0_1 = TimeSpan::new(TimePoint::from_msecs(0), TimePoint::from_secs(1.34)); let time_span_0_2 = TimeSpan::new(TimePoint::from_msecs(0), TimePoint::from_secs(2.34)); let time_span_1_2 = TimeSpan::new(TimePoint::from_msecs(1245), TimePoint::from_secs(2.34)); assert_ne!(time_span_0_1, time_span_0_2); assert_ne!(time_span_0_2, time_span_1_2); } } subtile-0.1.9/src/util.rs000066400000000000000000000010301464425311100152750ustar00rootroot00000000000000//! Miscellaneous utilities. use std::fmt; /// Wrapper to force a `&[u8]` to display as nicely-formatted hexadecimal /// bytes with only the the first line or so of bytes shown. pub struct BytesFormatter<'a>(pub &'a [u8]); impl<'a> fmt::Debug for BytesFormatter<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let BytesFormatter(bytes) = *self; for byte in bytes.iter().take(16) { write!(f, "{byte:02x} ")?; } write!(f, "({} bytes)", bytes.len())?; Ok(()) } } subtile-0.1.9/src/vobsub/000077500000000000000000000000001464425311100152605ustar00rootroot00000000000000subtile-0.1.9/src/vobsub/idx.rs000066400000000000000000000070171464425311100164170ustar00rootroot00000000000000//! Parse a file in `*.idx` format. use log::trace; use once_cell::sync::Lazy; use regex::Regex; use std::{ fs, io::{self, prelude::*, BufReader}, path::Path, }; use super::{palette, sub, IResultExt, Palette, VobSubError}; /// A `*.idx` file describing the subtitles in a `*.sub` file. #[derive(Debug)] pub struct Index { // Frame size. //size: Size, /// The colors used for the subtitles. palette: Palette, /// Our compressed subtitle data. sub_data: Vec, } impl Index { /// Open an `*.idx` file and the associated `*.sub` file. #[profiling::function] pub fn open>(path: P) -> Result { let path = path.as_ref(); let mkerr_idx = |source| VobSubError::Io { source, path: path.into(), }; let f = fs::File::open(path).map_err(mkerr_idx)?; let input = io::BufReader::new(f); let palette = read_palette(input, &mkerr_idx)?; let mut sub_path = path.to_owned(); sub_path.set_extension("sub"); let sub_path = sub_path.as_path(); let mut sub = fs::File::open(sub_path).map_err(|source| VobSubError::Io { source, path: sub_path.into(), })?; let mut sub_data = vec![]; sub.read_to_end(&mut sub_data) .map_err(|source| VobSubError::Io { source, path: sub_path.into(), })?; Ok(Self { palette, sub_data }) } /// Create an Index from a palette and sub data #[must_use] pub fn init(palette: Palette, sub_data: Vec) -> Self { Self { palette, sub_data } } /// Get the palette associated with this `*.idx` file. #[must_use] pub const fn palette(&self) -> &Palette { &self.palette } /// Iterate over the subtitles associated with this `*.idx` file. #[must_use] pub fn subtitles(&self) -> sub::Subtitles { sub::subtitles(&self.sub_data) } } /// Read the palette in .idx file content #[profiling::function] pub fn read_palette(mut input: BufReader, mkerr: &Err) -> Result where T: std::io::Read, Err: Fn(io::Error) -> VobSubError, { static KEY_VALUE: Lazy = Lazy::new(|| Regex::new("^([A-Za-z/ ]+): (.*)").unwrap()); let mut palette_val: Option = None; let mut buf = String::with_capacity(256); while input.read_line(&mut buf).map_err(mkerr)? > 0 { let line = buf.trim_end(); if let Some(cap) = KEY_VALUE.captures(line) { let key = cap.get(1).unwrap().as_str(); let val = cap.get(2).unwrap().as_str(); match key { "palette" => { palette_val = Some( palette(val.as_bytes()) .to_result_no_rest() .map_err(VobSubError::PaletteError)?, ); } _ => trace!("Unimplemented idx key: {}", key), } } buf.clear(); } let palette = palette_val.ok_or(VobSubError::MissingKey("palette"))?; Ok(palette) } #[cfg(test)] mod tests { use image::Rgb; use crate::vobsub::Index; #[test] fn parse_index() { env_logger::init(); let idx = Index::open("./fixtures/example.idx").unwrap(); //assert_eq!(idx.size(), Size { w: 1920, h: 1080 }); assert_eq!(idx.palette()[0], Rgb([0x00, 0x00, 0x00])); assert_eq!(idx.palette()[15], Rgb([0x11, 0xbb, 0xbb])); } } subtile-0.1.9/src/vobsub/img.rs000066400000000000000000000076561464425311100164200ustar00rootroot00000000000000//! Run-length encoded image format for subtitles. use cast; use log::trace; use nom::{ bits::complete::{tag as tag_bits, take as take_bits}, branch::alt, combinator::value, sequence::{preceded, Tuple}, IResult, }; use thiserror::Error; use super::IResultExt; use crate::{content::Size, util::BytesFormatter}; use super::NomError; /// Errors of `vobsub` img management. #[derive(Error, Debug)] pub enum Error { /// If there is more data to write than the space in output. #[error("output parameter is too small (size:{output_size}) for write scanline data (size:{data_size})")] ToSmallOutput { data_size: usize, output_size: usize, }, /// If index value is bigger than the image width. #[error("Scan line is longer than image width: [{x},{width}]")] ScanLineLongerThanWidth { x: usize, width: usize }, /// Forward scan line parsind error. #[error("Parsing scan line failed")] ScanLineParsing(#[source] NomError), } /// A run-length encoded value. #[derive(Debug)] struct Rle { /// The number of times to repeat this value. A value of 0 indicates that /// we should fill to the end of the line. cnt: u16, /// The value to repeat. This is 2 bits wide. val: u8, } /// Parse the count for a `Rle`. fn count(input: (&[u8], usize)) -> IResult<(&[u8], usize), u16> { // Fill to end of line. let end_of_line = value(0, tag_bits(0, 14u16)); // Count for 4-nibble RLE. let count4 = preceded(tag_bits(0, 6u8), take_bits(8u16)); // Count for 3-nibble RLE. let count3 = preceded(tag_bits(0, 4u8), take_bits(6u16)); // Count for 2-nibble RLE. let count2 = preceded(tag_bits(0, 2u8), take_bits(4u16)); // Count for 1-nibble RLE. let count1 = take_bits(2u16); alt((end_of_line, count4, count3, count2, count1))(input) } /// Parse an `Rle`. fn rle(input: (&[u8], usize)) -> IResult<(&[u8], usize), Rle> { let take_val = take_bits(2u8); let (input, (cnt, val)) = (count, take_val).parse(input)?; Ok((input, Rle { cnt, val })) } /// Decompress the scan-line `input` into `output`, returning the number of /// input bytes consumed. fn scan_line(input: &[u8], output: &mut [u8]) -> Result { trace!("scan line starting with {:?}", BytesFormatter(input)); let width = output.len(); let mut x = 0; let mut pos = (input, 0); while x < width { let (new_pos, run) = rle(pos).to_result().map_err(Error::ScanLineParsing)?; //trace!("RLE: {:?}", &run); pos = new_pos; let count = if run.cnt == 0 { width - x } else { cast::usize(run.cnt) }; if x + count > output.len() { return Err(Error::ToSmallOutput { data_size: x + count, output_size: output.len(), }); } output[x..x + count].fill(run.val); x += count; } if x > width { return Err(Error::ScanLineLongerThanWidth { x, width }); } // Round up to the next full byte. if pos.1 > 0 { pos = (&pos.0[1..], 0); } Ok(input.len() - pos.0.len()) } /// Decompress a run-length encoded image, and return a vector in row-major /// order, starting at the upper-left and scanning right and down, with one /// byte for each 2-bit value. #[profiling::function] pub fn decompress(size: Size, data: [&[u8]; 2]) -> Result, Error> { trace!( "decompressing image {:?}, max: [0x{:x}, 0x{:x}]", &size, data[0].len(), data[1].len() ); let mut img = vec![0; size.w * size.h]; let mut offsets = [0; 2]; for y in 0..size.h { let odd = y % 2; trace!("line {:?}, offset 0x{:x}", y, offsets[odd]); let consumed = scan_line( &data[odd][offsets[odd]..], &mut img[y * size.w..(y + 1) * size.w], )?; offsets[odd] += consumed; } // TODO: Warn if we didn't consume everything. Ok(img) } subtile-0.1.9/src/vobsub/mod.rs000066400000000000000000000176701464425311100164200ustar00rootroot00000000000000//! This module reads DVD subtitles in `VobSub` format. These are typically //! stored as two files: an `*.idx` file summarizing the subtitles, and an //! MPEG-2 Program Stream containing the actual subtitle packets. //! //! ## Example code //! //! ``` //! extern crate image; //! extern crate subtile; //! //! let idx = subtile::vobsub::Index::open("./fixtures/example.idx").unwrap(); //! for sub in idx.subtitles() { //! let sub = sub.unwrap(); //! println!("Time: {:0.3}-{:0.3}", sub.start_time(), sub.end_time()); //! println!("Always show: {:?}", sub.force()); //! let area = sub.area(); //! println!("At: {}, {}", area.left(), area.top()); //! println!("Size: {}x{}", area.width(), area.height()); //! let img: image::RgbaImage = sub.to_image(idx.palette()); //! //! // You can save or manipulate `img` using the APIs provided by the Rust //! // `image` crate. //! } //! ``` //! ## Limitations //! //! The initial version of this library is focused on extracting just the //! information shown above, and it does not have full support for all the //! options found in `*.idx` files. It also lacks support for rapidly //! finding the subtitle associated with a particular time during playback. //! //! ## Background & References //! //! `VobSub` subtitles consist of a simple textual `*.idx` file, and a binary //! `*.sub` file. The binary `*.sub` file is essentially an MPEG-2 Program //! Stream containing Packetized Elementary Stream data, but only for a //! single subtitle track. //! //! Useful references include: //! //! - [Program Stream](https://en.wikipedia.org/wiki/MPEG_program_stream) (PS) //! - [Packetized Elementary Stream][PES] (PES) //! - [DVD subtitles](http://sam.zoy.org/writings/dvd/subtitles/) //! - [System Time Clock](http://www.bretl.com/mpeghtml/STC.HTM) //! //! [PES]: http://dvd.sourceforge.net/dvdinfo/pes-hdr.html //! //! There are also any number of open source implementations of subtitles //! decoders which might be useful once you get past the Program Stream and //! PES wrappers. //! //! There are two closely-related formats that this library could be //! extended to parse without too much work: //! //! - Subtitles embedded in DVD-format video. These should contain the //! same subtitle packet format, but the `*.idx` file is replaced by data //! stored in an `IFO` file. //! - Subtitles stored in the Matroska container format. Again, these use //! the same basic subtitle format, but the `*.idx` file is replaced by //! an internal, stripped-down version of the same data in text format. //! mod idx; mod img; mod mpeg2; mod palette; mod probe; mod sub; pub use self::{ idx::{read_palette, Index}, palette::{palette, Palette}, probe::{is_idx_file, is_sub_file}, sub::{subtitles, ErrorMissing, Subtitle, Subtitles}, }; use crate::content::ContentError; use nom::{IResult, Needed}; use std::{fmt, io, path::PathBuf}; use thiserror::Error; /// Error for `VobSub` handling. #[derive(Debug, Error)] pub enum VobSubError { /// Content Error #[error("Error with data")] Content(#[from] ContentError), /// We were unable to find a required key in an `*.idx` file. #[error("Could not find required key '{0}'")] MissingKey(&'static str), /// We could not parse a value. #[error("Could not parse: {0}")] Parse(String), /// If invalid number of palette entries found. #[error("Palette must have 16 entries, found '{0}' one")] PaletteInvalidEntriesNumbers(usize), /// Parsing of palette in idx file failed. #[error("Error during palette pasing from .idx file")] PaletteError(#[source] NomError), /// If Scan line offsets values are not correct. #[error("invalid scan line offsets : start 0 {start_0}, start 1 {start_1}, end {end}")] InvalidScanLineOffsets { /// Start 0 start_0: usize, /// Start 1 start_1: usize, /// End end: usize, }, /// If the buffer is too Small for parsing a 16-bits value. #[error("unexpected end of buffer while parsing 16-bit size")] BufferTooSmallForU16, /// If the buffer is too small to parse a subtitle. #[error("unexpected end of subtitle data")] UnexpectedEndOfSubtitleData, /// If an error happen during `Control sequence` parsing. #[error("Error with Control sequence parsing.")] ControlSequence(#[source] NomError), /// If the control offset value tried to leads backwards. #[error("control offset value tried to leads backwards")] ControlOffsetWentBackwards, /// If `control offset` is bigger than packet size. #[error("control offset is 0x{offset:x}, but packet is only 0x{packet:x} bytes")] ControlOffsetBiggerThanPacket { /// Control offset offset: usize, /// Packet size packet: usize, }, /// If an error happen during `PES Packet` parsing. #[error("PES packet parsing.")] PESPacket(#[source] NomError), /// If the `control packet` is incmplete #[error("Incomplete control packet")] IncompleteControlPacket, /// Packet is too short, not bigger to read his size. #[error("Packet is too short")] PacketTooShort, /// If timing info for Subtitle is missing. #[error("found subtitle without timing into")] MissingTimingForSubtitle, /// Missing data from parsing to construct a subtitle. #[error("Missing during subtitle parsing")] MissingSubtitleParsing(#[from] ErrorMissing), /// We could not process a subtitle image. #[error("Could not process subtitle image: {0}")] Image(#[from] img::Error), /// Io error on a path. #[error("Io error on '{path}'")] Io { /// Source error source: io::Error, /// Path of the file we tried to read path: PathBuf, }, } /// Error from `nom` handling #[derive(Debug, Error)] pub enum NomError { /// We have leftover input that we didn't expect. #[error("Unexpected extra input")] UnexpectedInput, /// Our input data ended sooner than we expected. #[error("Incomplete input: '{0:?}' needed.")] IncompleteInput(Needed), /// An error happend during parsing #[error("Error from nom : {0}")] Error(String), /// And Failure happend during parsing #[error("Failure from nom : {0}")] Failure(String), } /// Extend `IResult` management, and convert to [`Result`] with [`NomError`] pub trait IResultExt { /// Convert an `IResult` to Result<_, `NomError`> and check than the buffer is empty after parsing. /// # Errors /// Forward `Error` and `Failure` from nom, and return `UnexpectedInput` if the buffer is not empty after parsing. fn to_result_no_rest(self) -> Result; /// Convert an `IResult` to Result<_, `NomError`> /// # Errors /// Forward `Error` and `Failure` from nom. fn to_result(self) -> Result<(I, O), NomError>; } impl IResultExt for IResult { fn to_result_no_rest(self) -> Result { match self { Self::Ok((rest, val)) => { if rest == I::default() { Ok(val) } else { Err(NomError::UnexpectedInput) } } Self::Err(err) => match err { nom::Err::Incomplete(needed) => Err(NomError::IncompleteInput(needed)), nom::Err::Error(err) => Err(NomError::Error(format!("{err:?}"))), nom::Err::Failure(err) => Err(NomError::Failure(format!("{err:?}"))), }, } } fn to_result(self) -> Result<(I, O), NomError> { match self { Self::Ok((rest, val)) => Ok((rest, val)), Self::Err(err) => match err { nom::Err::Incomplete(needed) => Err(NomError::IncompleteInput(needed)), nom::Err::Error(err) => Err(NomError::Error(format!("{err:?}"))), nom::Err::Failure(err) => Err(NomError::Failure(format!("{err:?}"))), }, } } } subtile-0.1.9/src/vobsub/mpeg2/000077500000000000000000000000001464425311100162725ustar00rootroot00000000000000subtile-0.1.9/src/vobsub/mpeg2/clock.rs000066400000000000000000000053661464425311100177450ustar00rootroot00000000000000use nom::{ bits::complete::{tag, take}, sequence::Tuple, IResult, }; use std::fmt; /// This represents the 90kHz, 33-bit [System Time Clock][STC] (STC) and /// the 9-bit STC extension value, which represents 1/300th of a tick. /// /// [STC]: http://www.bretl.com/mpeghtml/STC.HTM #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Clock { value: u64, } impl Clock { /// Given a 33-bit System Time Clock value, construct a new `Clock` /// value. pub const fn base(stc: u64) -> Self { Self { value: stc << 9 } } /// Return a new `Clock` value, setting the 9-bit extension to the /// specified value. pub fn with_ext(self, ext: u16) -> Self { Self { value: self.value & !0x1f | u64::from(ext), } } /// Convert a `Clock` value to seconds. pub fn as_seconds(self) -> f64 { let base = (self.value >> 9) as f64; let ext = (self.value & 0x1F) as f64; (base + ext / 300.0) / 90000.0 } } impl fmt::Display for Clock { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = self.as_seconds(); let h = (s / 3600.0).trunc(); s %= 3600.0; let m = (s / 60.0).trunc(); s %= 60.0; write!(f, "{h}:{m:02}:{s:1.3}") } } /// Parse a 33-bit `Clock` value with 3 marker bits, consuming 36 bits. pub fn clock(i: (&[u8], usize)) -> IResult<(&[u8], usize), Clock> { let marker = tag(0b1, 1usize); let hi_p = take(3usize); let mid_p = take(15usize); let lo_p = take(15usize); let (input, (hi, _, mid, _, lo, _)): ((&[u8], usize), (u64, _, u64, _, u64, _)) = (hi_p, &marker, mid_p, &marker, lo_p, &marker).parse(i)?; let clock = hi << 30 | mid << 15 | lo; Ok((input, Clock::base(clock))) } /// Parse a 33-bit `Clock` value plus a 9-bit extension and 4 marker bits, /// consuming 46 bits. pub fn clock_and_ext(input: (&[u8], usize)) -> IResult<(&[u8], usize), Clock> { let ext_bits = take(9u16); let clock_tag = tag(0b1, 1u8); let (input, (clock, ext, _)) = (clock, ext_bits, clock_tag).parse(input)?; Ok((input, clock.with_ext(ext))) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_clock() { use nom::IResult; assert_eq!( clock((&[0x44, 0x02, 0xc4, 0x82, 0x04][..], 2)), IResult::Ok(( (&[0x04][..], 6), Clock::base(0b0_0000_0000_0010_1100_0001_0000_0100_0000) )) ); assert_eq!( clock_and_ext((&[0x44, 0x02, 0xc4, 0x82, 0x04, 0xa9][..], 2)), IResult::Ok(( (&[][..], 0), Clock::base(0b0_0000_0000_0010_1100_0001_0000_0100_0000).with_ext(0b0_0101_0100) )) ); } } subtile-0.1.9/src/vobsub/mpeg2/mod.rs000066400000000000000000000002651464425311100174220ustar00rootroot00000000000000//! The `*.sub` portion of `VobSub` subtitles is packaged in MPEG-2 Program //! Stream packets, which we have some limited support for parsing. mod clock; pub mod pes; pub mod ps; subtile-0.1.9/src/vobsub/mpeg2/pes.rs000066400000000000000000000244311464425311100174330ustar00rootroot00000000000000//! # MPEG-2 Packetized Elementary Streams (PES) //! //! These packets are nested inside the MPEG-2 Program Stream packets found //! in a `*.sub` file. use nom::{ bits, bits::complete::{tag as tag_bits, take}, branch::alt, bytes::complete::tag as tag_bytes, combinator::{map, rest, value}, multi::length_value, number::complete::{be_u16, be_u8}, //do_parse, length_value, named, rest, sequence::Tuple, IResult, }; use std::fmt; use super::clock::{clock, Clock}; use crate::util::BytesFormatter; /// Possible combinations of PTS and DTS data which might appear inside a /// PES header. /// /// See the [PES header documentation][PES] for details. /// /// [PES]: http://dvd.sourceforge.net/dvdinfo/pes-hdr.html #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum PtsDtsFlags { /// No time stamps. #[default] None, /// Presentation Time Stamp only. Pts, /// Presentation and Decode Time Stamps. PtsDts, } /// Parse PTS & DTS flags in a PES packet header. Consumes two bits. fn pts_dts_flags(input: (&[u8], usize)) -> IResult<(&[u8], usize), PtsDtsFlags> { alt(( value(PtsDtsFlags::None, tag_bits(0b00, 2u8)), value(PtsDtsFlags::Pts, tag_bits(0b10, 2u8)), value(PtsDtsFlags::PtsDts, tag_bits(0b11, 2u8)), ))(input) } /// Presentation and Decode Time Stamps, if available. #[derive(Debug, PartialEq, Eq)] pub struct PtsDts { /// Presentation Time Stamp. pub pts: Clock, /// Decode Time Stamp. pub dts: Option, } /// Helper for `pts_dts`. Parses the PTS-only case. fn pts_only(input: &[u8]) -> IResult<&[u8], PtsDts> { bits(|input| { let tag_parse = tag_bits(0b0010, 4u8); let (input, (_, pts)) = (tag_parse, clock).parse(input)?; Ok((input, PtsDts { pts, dts: None })) })(input) } /// Helper for `pts_dts`. Parses the PTS and DTS case. fn pts_and_dts(input: &[u8]) -> IResult<&[u8], PtsDts> { bits(|input| { let parse_tag = tag_bits(0b0010, 4u8); let (input, (_, pts, _, dts)): ((&[u8], usize), (_, _, _, _)) = (&parse_tag, clock, &parse_tag, clock).parse(input)?; Ok(( input, PtsDts { pts, dts: Some(dts), }, )) })(input) } /// Parse a `PtsDts` value in the format specified by `flags`. fn pts_dts(i: &[u8], flags: PtsDtsFlags) -> IResult<&[u8], Option> { match flags { PtsDtsFlags::None => IResult::Ok((i, None)), PtsDtsFlags::Pts => pts_only(i).map(|(i, pts)| (i, Some(pts))), PtsDtsFlags::PtsDts => pts_and_dts(i).map(|(i, ptsdts)| (i, Some(ptsdts))), } } /// Flags specifying which header data fields are present. #[derive(Debug, Default, PartialEq, Eq)] pub struct HeaderDataFlags { pub pts_dts_flags: PtsDtsFlags, pub escr_flag: bool, pub es_rate_flag: bool, pub dsm_trick_mode_flag: bool, pub additional_copy_info_flag: bool, pub crc_flag: bool, pub extension_flag: bool, } /// Deserialize a single Boolean flag bit. fn bool_flag(input: (&[u8], usize)) -> IResult<(&[u8], usize), bool> { map(|input| bits::complete::take(1u8)(input), |b: u8| b == 1)(input) } /// Deserialize `HeaderDataFlags` fn header_data_flags(input: &[u8]) -> IResult<&[u8], HeaderDataFlags> { bits(|input| { let ( input, ( pts_dts_flags, escr_flag, es_rate_flag, dsm_trick_mode_flag, additional_copy_info_flag, crc_flag, extension_flag, ), ) = ( pts_dts_flags, bool_flag, bool_flag, bool_flag, bool_flag, bool_flag, bool_flag, ) .parse(input)?; Ok(( input, HeaderDataFlags { pts_dts_flags, escr_flag, es_rate_flag, dsm_trick_mode_flag, additional_copy_info_flag, crc_flag, extension_flag, }, )) })(input) } /// Header data fields. #[non_exhaustive] #[derive(Debug, Default, PartialEq, Eq)] pub struct HeaderData { pub flags: HeaderDataFlags, pub pts_dts: Option, } /// Parse PES header data, including the preceding flags and length bytes. fn header_data(input: &[u8]) -> IResult<&[u8], HeaderData> { // Grab the flags from our flag byte with header_data_flags. let (input, flags) = header_data_flags(input)?; // Grab a single length byte, read that many bytes, and recursively // call `header_data_fields` to do the actual parse. This ensures // that if `header_data_fields` doesn't parse all the header data, // we discard the rest before continuing. let (input, pts_dts) = length_value(be_u8, |input| pts_dts(input, flags.pts_dts_flags))(input)?; Ok((input, HeaderData { flags, pts_dts })) } /// A [Packetized Elementary Stream][pes] header, not including the /// `HeaderData` information (which is parsed separately). /// /// [pes]: http://dvd.sourceforge.net/dvdinfo/pes-hdr.html #[derive(Debug, Default, PartialEq, Eq)] pub struct Header { pub scrambling_control: u8, pub priority: bool, pub data_alignment_indicator: bool, pub copyright: bool, pub original: bool, } /// Parse the first PES header byte after the length. fn header(input: &[u8]) -> IResult<&[u8], Header> { bits(|input| { let tag_parse = tag_bits(0b10, 2u8); let take_scrambling = take(2u8); let ( input, (_, scrambling_control, priority, data_alignment_indicator, copyright, original), ) = ( tag_parse, take_scrambling, bool_flag, bool_flag, bool_flag, bool_flag, ) .parse(input)?; Ok(( input, Header { scrambling_control, priority, data_alignment_indicator, copyright, original, }, )) })(input) } /// A [Packetized Elementary Stream][pes] packet. /// /// [pes]: http://dvd.sourceforge.net/dvdinfo/pes-hdr.html #[derive(PartialEq, Eq)] pub struct Packet<'a> { pub header: Header, pub header_data: HeaderData, pub substream_id: u8, pub data: &'a [u8], } impl<'a> fmt::Debug for Packet<'a> { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.debug_struct("Packet") .field("header", &self.header) .field("header_data", &self.header_data) .field("substream_id", &self.substream_id) .field("data", &BytesFormatter(self.data)) .finish() } } fn packet_helper(input: &[u8]) -> IResult<&[u8], Packet> { let (input, (header, header_data, substream_id, data)) = (header, header_data, be_u8, rest).parse(input)?; Ok(( input, Packet { header, header_data, substream_id, data, }, )) } pub fn packet(input: &[u8]) -> IResult<&[u8], Packet> { let packet_tag = tag_bytes(&[0x00, 0x00, 0x01, 0xbd]); let packet_data = length_value(be_u16, packet_helper); let (input, (_, packet)) = (packet_tag, packet_data).parse(input)?; Ok((input, packet)) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_pts_dts_flags() { assert_eq!( pts_dts_flags((&[0b00][..], 6)), IResult::Ok(((&[][..], 0), PtsDtsFlags::None)) ); assert_eq!( pts_dts_flags((&[0b10][..], 6)), IResult::Ok(((&[][..], 0), PtsDtsFlags::Pts)) ); assert_eq!( pts_dts_flags((&[0b11][..], 6)), IResult::Ok(((&[][..], 0), PtsDtsFlags::PtsDts)) ); } #[test] fn parse_pts_dts() { assert_eq!( pts_dts(&[][..], PtsDtsFlags::None), IResult::Ok((&[][..], None)) ); assert_eq!( pts_dts(&[0x21, 0x00, 0xab, 0xe9, 0xc1][..], PtsDtsFlags::Pts), IResult::Ok(( &[][..], Some(PtsDts { pts: Clock::base(2_815_200), dts: None, }) )) ); } #[test] fn parse_header_data_flags() { assert_eq!( header_data_flags(&[0x80][..]), IResult::Ok(( &[][..], HeaderDataFlags { pts_dts_flags: PtsDtsFlags::Pts, ..HeaderDataFlags::default() } )) ); } #[test] fn parse_header_data() { assert_eq!( header_data(&[0x00, 0x00][..]), IResult::Ok((&[][..], HeaderData::default())) ); assert_eq!( header_data(&[0x80, 0x05, 0x21, 0x00, 0xab, 0xe9, 0xc1][..]), IResult::Ok(( &[][..], HeaderData { flags: HeaderDataFlags { pts_dts_flags: PtsDtsFlags::Pts, ..HeaderDataFlags::default() }, pts_dts: Some(PtsDts { pts: Clock::base(2_815_200), dts: None, }), ..HeaderData::default() } )) ); } #[test] fn parse_packet() { let input = &[ 0x00, 0x00, 0x01, 0xbd, 0x00, 0x10, 0x81, 0x80, 0x05, 0x21, 0x00, 0xab, 0xe9, 0xc1, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, ][..]; let expected = Packet { header: Header { original: true, ..Header::default() }, header_data: HeaderData { flags: HeaderDataFlags { pts_dts_flags: PtsDtsFlags::Pts, ..HeaderDataFlags::default() }, pts_dts: Some(PtsDts { pts: Clock::base(2_815_200), dts: None, }), ..HeaderData::default() }, substream_id: 0x20, data: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], }; assert_eq!(packet(input), IResult::Ok((&[0xff][..], expected))); } } subtile-0.1.9/src/vobsub/mpeg2/ps.rs000066400000000000000000000125751464425311100172740ustar00rootroot00000000000000//! # MPEG-2 Program Streams (PS) //! //! This is the container format used at the top-level of a `*.sub` file. use log::{debug, trace, warn}; use nom::{ bits::{ bits, complete::{tag as tag_bits, take as take_bits}, }, bytes::complete::tag as tag_bytes, sequence::Tuple, IResult, }; use std::fmt; use super::{ clock::{clock_and_ext, Clock}, pes, }; use crate::vobsub::{NomError, VobSubError}; /// A parsed [MPEG-2 Program Stream header][MPEG-PS] (MPEG-PS). /// /// [MPEG-PS]: https://en.wikipedia.org/wiki/MPEG_program_stream #[derive(Debug, PartialEq, Eq)] pub struct Header { /// The System Clock Reference (SCR) and SCR extension field. pub scr: Clock, /// The bit rate, in units of 50 bytes per second. pub bit_rate: u32, } impl fmt::Display for Header { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "[PS packet @ {}, {} kbps]", self.scr, (self.bit_rate * 50 * 8) / 1024 ) } } /// Parse a Program Stream header. pub fn header(input: &[u8]) -> IResult<&[u8], Header> { // Sync bytes. let tag1 = tag_bytes(&[0x00, 0x00, 0x01, 0xba]); // 10-byte header. let header_parse = bits(|input| { // MPEG-2 version tag. let tag_mpeg2 = tag_bits(0b01, 2u8); // Bit rate let bit_rate = take_bits(22u32); // Marker bits. let marker_bits = tag_bits(0b11, 2u8); // Reserved. let reserved = take_bits::<_, u8, u8, nom::error::Error<(&[u8], usize)>>(5u8); // Number of bytes of stuffing. let stuffing_length = take_bits::<_, usize, usize, nom::error::Error<(&[u8], usize)>>(3usize); // clock_and_ext: System Clock Reference. let (input, (_, scr, bit_rate, _, _, stuffing_length)) = ( tag_mpeg2, clock_and_ext, bit_rate, marker_bits, reserved, stuffing_length, ) .parse(input)?; // Stuffing bytes. We just want to ignore these, but use a // large enough type to prevent overflow panics when // fuzzing. let (input, _) = take_bits::<_, u64, _, _>(stuffing_length * 8)(input)?; Ok((input, Header { scr, bit_rate })) }); let (input, (_, header)) = (tag1, header_parse).parse(input)?; Ok((input, header)) } /// A [Packetized Elementary Stream][pes] packet with a Program Stream /// header. /// /// [pes]: http://dvd.sourceforge.net/dvdinfo/pes-hdr.html #[derive(Debug, PartialEq, Eq)] pub struct PesPacket<'a> { pub ps_header: Header, pub pes_packet: pes::Packet<'a>, } /// Parse a Program Stream packet and the following PES packet. pub fn pes_packet(input: &[u8]) -> IResult<&[u8], PesPacket> { let (input, (ps_header, pes_packet)) = (header, pes::packet).parse(input)?; Ok(( input, PesPacket { ps_header, pes_packet, }, )) } /// An iterator over all the PES packets in an MPEG-2 Program Stream. pub struct PesPackets<'a> { /// The remaining input to parse. remaining: &'a [u8], } impl<'a> Iterator for PesPackets<'a> { type Item = Result, VobSubError>; fn next(&mut self) -> Option { loop { // Search for the start of a ProgramStream packet. let needle = &[0x00, 0x00, 0x01, 0xba]; let start = self .remaining .windows(needle.len()) .position(|window| needle == window); if let Some(start) = start { // We found the start, so try to parse it. self.remaining = &self.remaining[start..]; match pes_packet(self.remaining) { // We found a packet! IResult::Ok((remaining, packet)) => { self.remaining = remaining; trace!("Decoded packet {:?}", &packet); return Some(Ok(packet)); } IResult::Err(err) => match err { // We have only a partial packet, and we hit the end of our // data. nom::Err::Incomplete(needed) => { self.remaining = &[]; warn!("Incomplete packet, need: {:?}", needed); return Some(Err(VobSubError::PESPacket(NomError::IncompleteInput( needed, )))); } // We got something that looked like a packet but // wasn't parseable. Log it and keep trying. nom::Err::Error(err) | nom::Err::Failure(err) => { self.remaining = &self.remaining[needle.len()..]; debug!("Skipping packet {:?}", &err); } }, } } else { // We didn't find the start of a packet. self.remaining = &[]; trace!("Reached end of data"); return None; } } } } /// Iterate over all the PES packets in an MPEG-2 Program Stream (or at /// least those which contain subtitles). pub const fn pes_packets(input: &[u8]) -> PesPackets { PesPackets { remaining: input } } subtile-0.1.9/src/vobsub/palette.rs000066400000000000000000000054431464425311100172720ustar00rootroot00000000000000use image::Rgb; use nom::{ bytes::complete::{tag, take_while_m_n}, combinator::map_res, multi::separated_list0, sequence::tuple, IResult, }; use super::VobSubError; /// Parse a single hexadecimal digit. fn from_hex(input: &[u8]) -> std::result::Result { let input = std::str::from_utf8(input).unwrap(); u8::from_str_radix(input, 16) } /// Parse a single byte hexadecimal byte. fn hex_primary(input: &[u8]) -> IResult<&[u8], u8> { map_res( take_while_m_n(2, 2, |c: u8| c.is_ascii_hexdigit()), from_hex, )(input) } /// Parse a 3-byte hexadecimal RGB color. fn hex_rgb(input: &[u8]) -> IResult<&[u8], Rgb> { let (input, (red, green, blue)) = tuple((hex_primary, hex_primary, hex_primary))(input)?; Ok((input, Rgb([red, green, blue]))) } /// The 16-color pallette used by the subtitles. pub type Palette = [Rgb; 16]; /// Parse a text as Palette /// # Errors /// /// Will return `Err` if the input don't have 16 entries. pub fn palette(input: &[u8]) -> IResult<&[u8], Palette> { let res = map_res(separated_list0(tag(b", "), hex_rgb), |vec: Vec>| { if vec.len() != 16 { return Err(VobSubError::PaletteInvalidEntriesNumbers(vec.len())); } // Coerce vector to known-size slice. Based on // http://stackoverflow.com/q/25428920/12089. let mut result = [Rgb([0, 0, 0]); 16]; <[Rgb; 16] as AsMut<_>>::as_mut(&mut result).clone_from_slice(&vec[0..16]); Ok(result) })(input); res } #[cfg(test)] mod tests { use super::*; use image::Rgb; #[test] fn parse_rgb() { use nom::IResult; assert_eq!( hex_rgb(&b"1234ab"[..]), IResult::Ok((&b""[..], Rgb::([0x12, 0x34, 0xab]))) ); } #[test] fn parse_palette() { use nom::IResult; let input = b"\ 000000, f0f0f0, cccccc, 999999, 3333fa, 1111bb, fa3333, bb1111, \ 33fa33, 11bb11, fafa33, bbbb11, fa33fa, bb11bb, 33fafa, 11bbbb"; assert_eq!(palette(input), { let palette = [ Rgb([0x00, 0x00, 0x00]), Rgb([0xf0, 0xf0, 0xf0]), Rgb([0xcc, 0xcc, 0xcc]), Rgb([0x99, 0x99, 0x99]), Rgb([0x33, 0x33, 0xfa]), Rgb([0x11, 0x11, 0xbb]), Rgb([0xfa, 0x33, 0x33]), Rgb([0xbb, 0x11, 0x11]), Rgb([0x33, 0xfa, 0x33]), Rgb([0x11, 0xbb, 0x11]), Rgb([0xfa, 0xfa, 0x33]), Rgb([0xbb, 0xbb, 0x11]), Rgb([0xfa, 0x33, 0xfa]), Rgb([0xbb, 0x11, 0xbb]), Rgb([0x33, 0xfa, 0xfa]), Rgb([0x11, 0xbb, 0xbb]), ]; IResult::Ok((&[][..], palette)) }); } } subtile-0.1.9/src/vobsub/probe.rs000066400000000000000000000030171464425311100167360ustar00rootroot00000000000000//! Try to guess the types of files on disk. use super::VobSubError; use std::{fs, io::Read, path::Path}; /// Internal helper function which looks for "magic" bytes at the start of /// a file. fn has_magic(path: &Path, magic: &[u8]) -> Result { let mkerr = |source| VobSubError::Io { source, path: path.into(), }; let mut f = fs::File::open(path).map_err(mkerr)?; let mut bytes = vec![0; magic.len()]; f.read_exact(&mut bytes).map_err(mkerr)?; Ok(magic == &bytes[..]) } /// Does the specified path appear to point to an `*.idx` file? /// # Errors /// /// Will return `Err` if the file can't be read. pub fn is_idx_file>(path: P) -> Result { has_magic(path.as_ref(), b"# VobSub index file") } /// Does the specified path appear to point to a `*.sub` file? /// /// Note that this may (or may not) return false positives for certain /// MPEG-2 related formats. /// /// # Errors /// /// Will return `Err` if the file can't be read. pub fn is_sub_file>(path: P) -> Result { has_magic(path.as_ref(), &[0x00, 0x00, 0x01, 0xba]) } #[cfg(test)] mod tests { use super::*; #[test] fn probe_idx_files() { assert!(is_idx_file("./fixtures/tiny.idx").unwrap()); assert!(!is_idx_file("./fixtures/tiny.sub").unwrap()); } #[test] fn probe_sub_files() { assert!(is_sub_file("./fixtures/tiny.sub").unwrap()); assert!(!is_sub_file("./fixtures/tiny.idx").unwrap()); } } subtile-0.1.9/src/vobsub/sub.rs000066400000000000000000000602541464425311100164260ustar00rootroot00000000000000//! # Subtitle data parsing. //! //! For background, see [this documentation on the DVD subtitle format][subs]. //! //! [subs]: http://sam.zoy.org/writings/dvd/subtitles/ use cast; use image::{ImageBuffer, Rgba, RgbaImage}; use log::{trace, warn}; use nom::{ bits::{bits, complete::take as take_bits}, branch::alt, bytes, bytes::complete::{tag as tag_bytes, take_until}, combinator::{map, value}, multi::{count, many_till}, number::complete::be_u16, sequence::{preceded, Tuple}, IResult, }; use std::{cmp::Ordering, fmt}; use thiserror::Error; use super::{img::decompress, mpeg2::ps, Palette, VobSubError}; use crate::{ content::{Area, AreaValues}, util::BytesFormatter, vobsub::IResultExt, }; /// The default time between two adjacent subtitles if no end time is /// provided. This is chosen to be a value that's usually representable in /// `SRT` format, barring rounding errors. const DEFAULT_SUBTITLE_SPACING: f64 = 0.001; /// The default length of a subtitle if no end time is provided and no /// subtitle follows immediately after. const DEFAULT_SUBTITLE_LENGTH: f64 = 5.0; /// Parse four 4-bit palette entries. fn palette_entries(input: &[u8]) -> IResult<&[u8], [u8; 4]> { let (input, vec) = bits(count( take_bits::<_, _, _, nom::error::Error<(&[u8], usize)>>(4usize), 4, ))(input)?; let mut result = [0; 4]; <[u8; 4] as AsMut<_>>::as_mut(&mut result).clone_from_slice(&vec[0..4]); Ok((input, result)) } /// Parse a 12-bit coordinate value. fn coordinate(input: (&[u8], usize)) -> IResult<(&[u8], usize), u16> { take_bits::<_, _, _, _>(12u8)(input) } /// Parse four 12-bit coordinate values as a rectangle (with right and /// bottom coordinates inclusive). fn area(input: &[u8]) -> IResult<&[u8], AreaValues> { bits(|input| { let (input, (x1, x2, y1, y2)) = (coordinate, coordinate, coordinate, coordinate).parse(input)?; Ok((input, AreaValues { x1, y1, x2, y2 })) })(input) } /// Parse a pair of 16-bit RLE offsets. fn rle_offsets(input: &[u8]) -> IResult<&[u8], [u16; 2]> { let (input, vec) = bits(count( take_bits::<_, _, _, nom::error::Error<(&[u8], usize)>>(16u16), 2, ))(input)?; Ok((input, [vec[0], vec[1]])) } /// Individual commands which may appear in a control sequence. #[derive(Clone, Debug, PartialEq, Eq)] enum ControlCommand<'a> { /// Should this subtitle be displayed even if subtitles are turned off? Force, /// We should start displaying the subtitle at the `date` for this /// `ControlSequence`. StartDate, /// We should stop displaying the subtitle at the `date` for this /// `ControlSequence`. StopDate, /// Map each of the 4 colors in this subtitle to a 4-bit palette. Palette([u8; 4]), /// Map each of the 4 colors in this subtitle to 4 bits of alpha /// channel data. Alpha([u8; 4]), /// Coordinates at which to display the subtitle. Coordinates(AreaValues), /// Offsets of first and second scan line in our data buffer. Note /// that the data buffer stores alternating scan lines separately, so /// these are the first line in each of the two chunks. RleOffsets([u16; 2]), /// Unsupported trailing data that we don't know how to parse. Unsupported(&'a [u8]), } /// Parse a single command in a control sequence. fn control_command(input: &[u8]) -> IResult<&[u8], ControlCommand> { alt(( value(ControlCommand::Force, tag_bytes(&[0x00])), value(ControlCommand::StartDate, tag_bytes(&[0x01])), value(ControlCommand::StopDate, tag_bytes(&[0x02])), map( preceded(tag_bytes(&[0x03]), palette_entries), ControlCommand::Palette, ), map( preceded(tag_bytes(&[0x04]), palette_entries), ControlCommand::Alpha, ), map( preceded(tag_bytes(&[0x05]), area), ControlCommand::Coordinates, ), map( preceded(tag_bytes(&[0x06]), rle_offsets), ControlCommand::RleOffsets, ), // We only capture this so we have something to log. Note that we // may not find the _true_ `ControlCommand::End` in this case, but // that doesn't matter, because we'll use the `next` field of // `ControlSequence` to find the next `ControlSequence`. map(take_until(&[0xff][..]), ControlCommand::Unsupported), ))(input) } /// The end of a control sequence. fn control_command_end(input: &[u8]) -> IResult<&[u8], &[u8]> { bytes::complete::tag(&[0xff])(input) } /// The control packet for a subtitle. #[derive(Debug, Clone, PartialEq, Eq)] struct ControlSequence<'a> { /// The time associated with this control sequence, specified in /// 1/100th of a second after the Presentation Time Stamp for this /// subtitle's packet. date: u16, /// The offset of the next control sequence, relative to ???. If this /// equals the offset of the current control sequence, this is the last /// control packet. next: u16, /// Individual commands in this sequence. commands: Vec>, } /// Parse a single control sequence. fn control_sequence(input: &[u8]) -> IResult<&[u8], ControlSequence> { let (input, (date, next, commands)) = ( be_u16, be_u16, many_till(control_command, control_command_end), ) .parse(input)?; Ok(( input, ControlSequence { date, next, commands: commands.0, }, )) } /// A single subtitle. #[derive(Clone, PartialEq)] pub struct Subtitle { /// Start time of subtitle, in seconds. start_time: f64, /// End time of subtitle, in seconds. This may be missing from certain /// subtitles. end_time: Option, /// Should this subtitle be shown even when subtitles are off? force: bool, /// Area coordinates at which to display the subtitle. area: Area, /// Map each of the 4 colors in this subtitle to a 4-bit palette. palette: [u8; 4], /// Map each of the 4 colors in this subtitle to 4 bits of alpha /// channel data. alpha: [u8; 4], /// Our decompressed image, stored with 2 bits per byte in row-major /// order, that can be used as indices into `palette` and `alpha`. raw_image: Vec, } impl Subtitle { /// Start time of subtitle, in seconds. #[must_use] pub const fn start_time(&self) -> f64 { self.start_time } /// End time of subtitle, in seconds. This may be missing from certain /// subtitles. /// # Panics /// Will panic if `end_time` is not set. As it should be set before returning subtitle. /// If happened `end_time` is called to soon, or a change has broken the intended operation. #[must_use] pub fn end_time(&self) -> f64 { self.end_time .expect("end time should have been set before returning subtitle") } /// Should this subtitle be shown even when subtitles are off? #[must_use] pub const fn force(&self) -> bool { self.force } /// Coordinates at which to display the subtitle. #[must_use] pub const fn area(&self) -> &Area { &self.area } /// Map each of the 4 colors in this subtitle to a 4-bit palette. #[must_use] pub const fn palette(&self) -> &[u8; 4] { &self.palette } /// Map each of the 4 colors in this subtitle to 4 bits of alpha /// channel data. #[must_use] pub const fn alpha(&self) -> &[u8; 4] { &self.alpha } /// Our decompressed image, stored with 2 bits per byte in row-major /// order, that can be used as indices into `palette` and `alpha`. #[must_use] pub fn raw_image(&self) -> &[u8] { &self.raw_image } /// Decompress to subtitle to an RBGA image. #[must_use] pub fn to_image(&self, palette: &Palette) -> RgbaImage { let width = cast::u32(self.area.width()); let height = cast::u32(self.area.height()); ImageBuffer::from_fn(width, height, |x, y| { let offset = cast::usize(y * width + x); // We need to subtract the raw index from 3 to get the same // results as everybody else. I found this by inspecting the // Handbrake subtitle decoding routines. let px = cast::usize(3 - self.raw_image[offset]); let rgb = palette[cast::usize(self.palette[px])].0; let a = self.alpha[px]; let aa = a << 4 | a; Rgba([rgb[0], rgb[1], rgb[2], aa]) }) } } impl fmt::Debug for Subtitle { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.debug_struct("Subtitle") .field("start_time", &self.start_time) .field("end_time", &self.end_time) .field("force", &self.force) .field("area", &self.area) .field("palette", &self.palette) .field("alpha", &self.alpha) .finish_non_exhaustive() } } /// Parse a single `u16` value from a buffer. We don't use `nom` for this /// because it has an inconvenient error type. fn parse_be_u16_as_usize(buff: &[u8]) -> Result<(&[u8], usize), VobSubError> { if buff.len() < 2 { Err(VobSubError::BufferTooSmallForU16) } else { Ok((&buff[2..], usize::from(buff[0]) << 8 | usize::from(buff[1]))) } } /// Errors for missing subtitle part after parsing. #[derive(Debug, Error)] pub enum ErrorMissing { /// No start time. #[error("no start time")] StartTime, /// No area coordinates #[error("no area coordinates")] Area, /// No palette #[error("no palette")] Palette, /// No alpha palette #[error("no alpha palette")] AlphaPalette, /// No RLE offsets #[error("no RLE offsets")] RleOffset, } /// Parse a subtitle. fn subtitle(raw_data: &[u8], base_time: f64) -> Result { // This parser is somewhat non-standard, because we need to work with // explicit offsets into `packet` in several places. // Figure out where our control data starts. if raw_data.len() < 2 { return Err(VobSubError::UnexpectedEndOfSubtitleData); } let (_, initial_control_offset) = parse_be_u16_as_usize(&raw_data[2..])?; // Declare data we want to collect from our control packets. let mut start_time = None; let mut end_time = None; let mut force = false; let mut area = None; let mut palette = None; let mut alpha = None; let mut rle_offsets = None; // Loop over the individual control sequences. let mut control_offset = initial_control_offset; loop { trace!("looking for control sequence at: 0x{:x}", control_offset); if control_offset >= raw_data.len() { return Err(VobSubError::ControlOffsetBiggerThanPacket { offset: control_offset, packet: raw_data.len(), }); } let control_data = &raw_data[control_offset..]; let (_, control) = control_sequence(control_data) .to_result() .map_err(VobSubError::ControlSequence)?; trace!("parsed control sequence: {:?}", &control); // Extract as much data as we can from this control sequence. let time = base_time + f64::from(control.date) / 100.0; for command in control.commands { match command { ControlCommand::Force => { force = true; } ControlCommand::StartDate => { start_time = start_time.or(Some(time)); } ControlCommand::StopDate => { end_time = end_time.or(Some(time)); } ControlCommand::Palette(p) => { palette = palette.or(Some(p)); } ControlCommand::Alpha(a) => { alpha = alpha.or(Some(a)); } ControlCommand::Coordinates(c) => { let cmd_area = Area::try_from(c)?; area = area.or(Some(cmd_area)); } ControlCommand::RleOffsets(r) => { rle_offsets = Some(r); } ControlCommand::Unsupported(b) => { warn!("unsupported control sequence: {:?}", BytesFormatter(b)); } } } // Figure out where to look for the next control sequence, // if any. let next_control_offset = cast::usize(control.next); match control_offset.cmp(&next_control_offset) { Ordering::Greater => { return Err(VobSubError::ControlOffsetWentBackwards); } Ordering::Equal => { // This points back at us, so we're the last packet. break; } Ordering::Less => { control_offset = next_control_offset; } } } // Make sure we found all the control commands that we expect. let start_time = start_time.ok_or(ErrorMissing::StartTime)?; let area = area.ok_or(ErrorMissing::Area)?; let palette = palette.ok_or(ErrorMissing::Palette)?; let alpha = alpha.ok_or(ErrorMissing::AlphaPalette)?; let rle_offsets = rle_offsets.ok_or(ErrorMissing::RleOffset)?; // Decompress our image. // // We know the starting points of each set of scan lines, but we don't // really know where they end, because encoders like to reuse bytes // that they're already using for something else. For example, the // last few bytes of the first set of scan lines may overlap with the // first bytes of the second set of scanlines, and the last bytes of // the second set of scan lines may overlap with the start of the // control sequence. For now, we limit it to the first two bytes of // the control packet, which are usually `[0x00, 0x00]`. (We might // actually want to remove `end` entirely here and allow the scan lines // to go to the end of the packet, but I've never seen that in // practice.) let start_0 = cast::usize(rle_offsets[0]); let start_1 = cast::usize(rle_offsets[1]); let end = cast::usize(initial_control_offset + 2); if start_0 > start_1 || start_1 > end { return Err(VobSubError::InvalidScanLineOffsets { start_0, start_1, end, }); } let image = decompress( area.size(), [&raw_data[start_0..end], &raw_data[start_1..end]], )?; // Return our parsed subtitle. let result = Subtitle { start_time, end_time, force, area, palette, alpha, raw_image: image, }; trace!("Parsed subtitle: {:?}", &result); Ok(result) } /// Like `?` and `try!`, but assume that we're working with /// `Option>` instead of `Result`, and pass through /// `None`. macro_rules! try_iter { ($e:expr) => { match $e { None => return None, Some(Err(e)) => return Some(Err(From::from(e))), Some(Ok(value)) => value, } }; } /// An internal iterator over subtitles. These subtitles may not have a /// valid `end_time`, so we'll try to fix them up before letting the user /// see them. struct SubtitlesInternal<'a> { pes_packets: ps::PesPackets<'a>, } impl<'a> Iterator for SubtitlesInternal<'a> { type Item = Result; fn next(&mut self) -> Option { profiling::scope!("SubtitlesInternal next"); // Get the PES packet containing the first chunk of our subtitle. let first: ps::PesPacket = try_iter!(self.pes_packets.next()); // Fetch useful information from our first packet. let pts_dts = match first.pes_packet.header_data.pts_dts { Some(v) => v, None => return Some(Err(VobSubError::MissingTimingForSubtitle)), }; let base_time = pts_dts.pts.as_seconds(); let substream_id = first.pes_packet.substream_id; // Figure out how many total bytes we'll need to collect from one // or more PES packets, and collect the first chunk into a buffer. if first.pes_packet.data.len() < 2 { return Some(Err(VobSubError::PacketTooShort)); } let wanted = usize::from(first.pes_packet.data[0]) << 8 | usize::from(first.pes_packet.data[1]); let mut sub_packet = Vec::with_capacity(wanted); sub_packet.extend_from_slice(first.pes_packet.data); // Keep fetching more packets until we have enough. while sub_packet.len() < wanted { // Get the next PES packet in the Program Stream. let next: ps::PesPacket = try_iter!(self.pes_packets.next()); // Make sure this is part of the same subtitle stream. This is // mostly just paranoia; I don't expect this to happen. if next.pes_packet.substream_id != substream_id { warn!( "Found subtitle for stream 0x{:x} while looking for 0x{:x}", next.pes_packet.substream_id, substream_id ); continue; } // Add the extra bytes to our buffer. sub_packet.extend_from_slice(next.pes_packet.data); } // Check to make sure we didn't get too _many_ bytes. Again, this // is paranoia. if sub_packet.len() > wanted { warn!( "Found 0x{:x} bytes of data in subtitle packet, wanted 0x{:x}", sub_packet.len(), wanted ); sub_packet.truncate(wanted); } // Parse our subtitle buffer. Some(subtitle(&sub_packet, base_time)) } } /// An iterator over subtitles. pub struct Subtitles<'a> { internal: SubtitlesInternal<'a>, prev: Option, } impl<'a> Iterator for Subtitles<'a> { type Item = Result; // This whole routine exists to make sure that `end_time` is set to a // useful value even if the subtitles themselves didn't supply one. // I'm not even sure this is valid, but it has been observed in the // wild. fn next(&mut self) -> Option { profiling::scope!("Subtitles next"); // If we don't currently have a previous subtitle, attempt to fetch // one. if self.prev.is_none() { match self.internal.next() { Some(Ok(sub)) => { self.prev = Some(sub); } other => return other, } } debug_assert!(self.prev.is_some()); match self.internal.next() { // We have a another subtitle! We want to return `self.prev` // and store the new subtitle as `self.prev`. Some(Ok(curr)) => { // `unwrap` is safe because of the invariant above. let mut prev = self.prev.take().unwrap(); if prev.end_time.is_none() { // Our subtitle has no end time, so end it just before // the next subtitle. let new_end = curr.start_time - DEFAULT_SUBTITLE_SPACING; let alt_end = prev.start_time + DEFAULT_SUBTITLE_LENGTH; prev.end_time = Some(new_end.min(alt_end)); } self.prev = Some(curr); Some(Ok(prev)) } // We encountered an error. We could, I suppose, attempt to // first return `self.prev` and save the error for next time, // but that's too much trouble. Some(Err(err)) => Some(Err(err)), // The only subtitle left to return is `self.prev`. None => { self.prev.take().map(|mut sub| { if sub.end_time.is_none() { // Our subtitle has no end time, and it's the last // subtitle, so just pick something. sub.end_time = Some(sub.start_time + DEFAULT_SUBTITLE_LENGTH); } Ok(sub) }) } } } } /// Return an iterator over the subtitles in this data stream. #[must_use] pub const fn subtitles(input: &[u8]) -> Subtitles { Subtitles { internal: SubtitlesInternal { pes_packets: ps::pes_packets(input), }, prev: None, } } #[cfg(test)] mod tests { use super::*; use crate::vobsub::idx; #[test] fn parse_palette_entries() { assert_eq!( palette_entries(&[0x03, 0x10][..]), IResult::Ok((&[][..], [0x00, 0x03, 0x01, 0x00])) ); } #[test] fn parse_control_sequence() { let input_1 = &[ 0x00, 0x00, 0x0f, 0x41, 0x01, 0x03, 0x03, 0x10, 0x04, 0xff, 0xf0, 0x05, 0x29, 0xb4, 0xe6, 0x3c, 0x54, 0x00, 0x06, 0x00, 0x04, 0x07, 0x7b, 0xff, ][..]; let expected_1 = ControlSequence { date: 0x0000, next: 0x0f41, commands: vec![ ControlCommand::StartDate, ControlCommand::Palette([0x0, 0x3, 0x1, 0x0]), ControlCommand::Alpha([0xf, 0xf, 0xf, 0x0]), ControlCommand::Coordinates(AreaValues { x1: 0x29b, x2: 0x4e6, y1: 0x3c5, y2: 0x400, }), ControlCommand::RleOffsets([0x0004, 0x077b]), ], }; assert_eq!( control_sequence(input_1), IResult::Ok((&[][..], expected_1)) ); let input_2 = &[0x00, 0x77, 0x0f, 0x41, 0x02, 0xff][..]; let expected_2 = ControlSequence { date: 0x0077, next: 0x0f41, commands: vec![ControlCommand::StopDate], }; assert_eq!( control_sequence(input_2), IResult::Ok((&[][..], expected_2)) ); // An out of order example. let input_3 = &[ 0x00, 0x00, 0x0b, 0x30, 0x01, 0x00, // ...other commands would appear here... 0xff, ][..]; let expected_3 = ControlSequence { date: 0x0000, next: 0x0b30, commands: vec![ControlCommand::StartDate, ControlCommand::Force], }; assert_eq!( control_sequence(input_3), IResult::Ok((&[][..], expected_3)) ); } #[test] fn parse_subtitles() { //use env_logger; use std::fs; use std::io::prelude::*; //let _ = env_logger::init(); let mut f = fs::File::open("./fixtures/example.sub").unwrap(); let mut buffer = vec![]; f.read_to_end(&mut buffer).unwrap(); let mut subs = subtitles(&buffer); let sub1 = subs.next().expect("missing sub 1").unwrap(); assert!(sub1.start_time - 49.4 < 0.1); assert!(sub1.end_time.unwrap() - 50.9 < 0.1); assert!(!sub1.force); assert_eq!( sub1.area, Area::try_from(AreaValues { x1: 750, y1: 916, x2: 1172, y2: 966 }) .unwrap() ); assert_eq!(sub1.palette, [0, 3, 1, 0]); assert_eq!(sub1.alpha, [15, 15, 15, 0]); subs.next().expect("missing sub 2").unwrap(); assert!(subs.next().is_none()); } #[test] fn parse_subtitles_from_subtitle_edit() { //use env_logger; use idx::Index; //let _ = env_logger::init(); let idx = Index::open("./fixtures/tiny.idx").unwrap(); let mut subs = idx.subtitles(); subs.next().expect("missing sub").unwrap(); assert!(subs.next().is_none()); } #[test] fn parse_fuzz_corpus_seeds() { //use env_logger; use idx::Index; //let _ = env_logger::init(); // Make sure these two fuzz corpus inputs still work, and that they // return the same subtitle data. let tiny = Index::open("./fixtures/tiny.idx") .unwrap() .subtitles() .next() .unwrap() .unwrap(); let split = Index::open("./fixtures/tiny-split.idx") .unwrap() .subtitles() .next() .unwrap() .unwrap(); assert_eq!(tiny, split); } }