pax_global_header00006660000000000000000000000064147407612670014530gustar00rootroot0000000000000052 comment=ebe198aee4e12b8b1ea8740263480ee9e7192a0d subtile-0.3.2/000077500000000000000000000000001474076126700132015ustar00rootroot00000000000000subtile-0.3.2/.github/000077500000000000000000000000001474076126700145415ustar00rootroot00000000000000subtile-0.3.2/.github/workflows/000077500000000000000000000000001474076126700165765ustar00rootroot00000000000000subtile-0.3.2/.github/workflows/code_check.yml000066400000000000000000000054731474076126700214010ustar00rootroot00000000000000name: CI - Code Checks & Tests run-name: Checks Rust code on ${{ github.event_name }} on: push: paths: - ".github/workflows/code_check.yml" - "Cargo.*" - "src/**" pull_request: paths: - ".github/workflows/code_check.yml" - "Cargo.*" - "src/**" env: CARGO_TERM_COLOR: always jobs: ci_code_checks_and_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: Check spelling of entire workspace id: typos_check if: $${{ always() }} uses: crate-ci/typos@master - 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 "|typos|${{ steps.typos_check.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.3.2/.github/workflows/release-plz.yml000066400000000000000000000036641474076126700215550ustar00rootroot00000000000000name: Release-plz permissions: pull-requests: write contents: write on: push: branches: - main jobs: release-plz-release: name: Release-plz release runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PLZ_TOKEN }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz uses: release-plz/action@v0.5 with: command: release env: GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} release-plz-pr: name: Release-plz PR runs-on: ubuntu-latest concurrency: group: release-plz-${{ github.ref }} cancel-in-progress: false steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.RELEASE_PLZ_TOKEN }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz id: release-plz-pr uses: release-plz/action@v0.5 with: command: release-pr env: GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Install cargo-readme run: cargo install cargo-readme - name: Update README with cargo-readme in the release PR env: GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} PR: ${{ steps.release-plz-pr.outputs.pr }} run: | set -e pr_number=${{ fromJSON(steps.release-plz-pr.outputs.pr).number }} if [[ -n "$pr_number" ]]; then gh pr checkout $pr_number cargo readme > README.md git add . git commit --amend --no-edit git push -f fi subtile-0.3.2/.gitignore000066400000000000000000000000161474076126700151660ustar00rootroot00000000000000/target *.origsubtile-0.3.2/.typos.toml000066400000000000000000000001651474076126700153340ustar00rootroot00000000000000[default.extend-words] # Don't correct the crate name subtile = "subtile" [files] extend-exclude = ["CHANGELOG.md"] subtile-0.3.2/CHANGELOG.md000066400000000000000000000114431474076126700150150ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.3.2](https://github.com/gwen-lg/subtile/compare/v0.3.1...v0.3.2) - 2025-01-12 ### Added - *(ods)* manage object data split in multiple segment ### Fixed - *(pgs)* skip_segment content in DecodeTimeOnly ### Other - *(release-plz)* update github action from v0.3.112 - *(ods)* create read_object_data function - *(ods)* move ObjectDataLength reading in dedicated function - *(ods)* move image size fields reading in a dedicated function - *(ods)* move last_in_sequence_flag field handling into a method - *(ods)* move object fields handling into a function - *(pgs)* add only_one.sup fixtures and associate test ## [0.3.1](https://github.com/gwen-lg/subtile/compare/v0.3.0...v0.3.1) - 2025-01-06 ### Fixed - setup a default palette if missing in index file - *(const)* add missing const keyword ### Other - use secrets.GITHUB_TOKEN not RELEASE_PLZ_TOKENT - remove useless intermediate variable in `read_palette` - add const for `palette` key - fix wokflows file path for triggering - *(clippy)* remove unnecessary closure for error creation - *(clippy)* remove explicit lifetimes than could be elided - *(cargo)* update crate thiserror to 2.0 - *(cargo)* update dependendies - include cargo readme management in release-plz workflow ## [0.3.0](https://github.com/gwen-lg/subtile/compare/v0.2.0...v0.3.0) - 2024-08-11 ### Added - *(pgs)* implement ToOcrImage for RleIndexedImage - *(pgs)* Add implementation of ToImage for pgs image - feat(): use `Borrow` for more generic pixel convert functions - *(image)* add pixel convert functions - *(pgs)* add size_hint implementation for SupParser - *(pgs)* add size_hint and implement ExactSizeIterator - *(pgs)* add pixels method on RleEncodedImage - *(pgs)* add pixel color convertion with genericity - *(pgs)* manage unexpected eof error in read_next_pixel - *(pgs)* add decoding of Rle PGS image - *(pgs)* set Palette in RleEncodedImage - *(pgs)* handle custom offset in Palette for PaletteEntries - *(pgs)* Add Palette struct to better handle PaletteEntries - *(pgs)* add PaletteDefinitionSegment parsing. - *(pgs)* add RleEncodedImage & impl SubtitleImage - *(pgs)* add ODS parsing - *(pgs)* add u24 type - *(pgs)* add DecoderTimeImage - *(pgs)* add segment header parsing - *(pgs)* add ReadExt extension trait. - *(pgs)* add SegmentTypeCode struct - *(pgs)* add blank implementation of Iterator for SupParser - *(pgs)* add from_file on SupParser - *(pgs)* add PgsDecoder trait for use by SupParser - *(pgs)* create SupParser struct - *(typos)* add .typos.toml conf ### Fixed - *(typos)* fix somes typos in doc, func name and data files ### Other - add 'pgs' as keyword for crate - *(pgs)* [**breaking**] use `seek_relative` to avoid buffer discard - cargo update - *(cargo)* move dependencies before lints setup - *(error)* add error and panic documentation - *(typos)* add `typos` step in code_check ci workflow - *(github)* fix typo in job name of code_check.yml - add space between `90` and `kHz` - use word image instead of img - fix link to VobsubParser struct - add backticks for specifics some term ## [0.2.0](https://github.com/gwen-lg/subtile/compare/v0.1.9...v0.2.0) - 2024-07-18 ### Added - *(image)* add trait ToImage for ImageBuffer generation - *(vobsub)* add genericity to `VobSubParser` - *(vobsub)* add a `VobSubDecoder` impl to keep only the TimeSpan - *(image)* add trait ToOcrImage and struct ToOcrImageOpt. - *(image)* add trait ImageArea and impl for ImageSize types - *(image)* add ImageSize trait and use it for VobSubIndexedImage - *(vobsub)* move image data from Subtile struct in a dedicated - *(vobsub)* add VobSubDecoder trait and use it ... - *(vobsub)* [**breaking**] create VobsubParser struct - add Default impl (derive) for time structs ### Other - add release-plz github workflow - *(vobsub)* remove useless `to_image` from VobSubIndexedImage - *(vobsub)* use `VobSubToImage` in vobsub example - *(vobsub)* create `VobSubToImage` struct who implement ToImage - *(vobsub)* add a test to parse only subtitles times - *(vobsub)* [**breaking**] remove Subtitle struct, - *(vobsub)* invert order of palette and alpha value after parsing - *(vobsub)* add VobSubOcrImage to create image addapted to OCR - *(vobsub)* add VobSubRleImage to be used by VobSub decoders - *(vobsub)* add struct VobSubRleImageData to ... - *(vobsub)* create a dedicated method for sub packet reading - *(vobsub)* move missing end_time out of iterator - some typo fixes and backticks added - make dump_images accept iterator of value - remove some useless use of cast - [**breaking**] rename SubError to SubtileError - Add Changelog file with only header subtile-0.3.2/Cargo.lock000066400000000000000000000304161474076126700151120ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[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.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytemuck" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "fdeflate" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", "num-traits", "png", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "iter_fixed" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "592ff74cdc6a923b2ae357dad7db2f5dcbf5d4ace9afcf3ab7c7f20b209fd949" [[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.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", "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.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "png" version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", "syn", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[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.3.2" dependencies = [ "cast", "env_logger", "glob", "image", "iter_fixed", "log", "nom", "once_cell", "profiling", "regex", "thiserror", ] [[package]] name = "syn" version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[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.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 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.3.2/Cargo.toml000066400000000000000000000036321474076126700151350ustar00rootroot00000000000000[package] name = "subtile" version = "0.3.2" 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", "vobsub", "pgs"] categories = [ "command-line-utilities", "encoding", "multimedia::encoding", "parser-implementations", ] license = "LGPL-3.0-or-later" rust-version = "1.80" [badges] maintenance = { status = "actively-developed" } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cast = "0.3" image = { version = "0.25", default-features = false, features = ["png"] } iter_fixed = "0.4" log = "0.4" nom = "7.1" once_cell = "1.20" profiling = "1.0" regex = "1.11" thiserror = "2.0" [dev-dependencies] env_logger = "0.11" glob = "0.3" [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" subtile-0.3.2/LICENSE.txt000066400000000000000000000167441474076126700150400ustar00rootroot00000000000000 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.3.2/README.md000066400000000000000000000021341474076126700144600ustar00rootroot00000000000000# subtile ## Current version: 0.3.2 ![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.3.2/status.svg)](https://deps.rs/crate/subtile/0.3.2) `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.3.2/README.tpl000066400000000000000000000006121474076126700146560ustar00rootroot00000000000000# {{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.3.2/fixtures/000077500000000000000000000000001474076126700150525ustar00rootroot00000000000000subtile-0.3.2/fixtures/example.idx000066400000000000000000000021271474076126700172150ustar00rootroot00000000000000# 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.3.2/fixtures/example.sub000066400000000000000000000300001474076126700172110ustar00rootroot00000000000000DĂ쁀! jB0H# 0# 0`# _ <_ VoKa̶V3a0a0Pa0ȶQ03a0a0Pa0ȶQ03+a0a0T/ a!k3 0#,V3 a3<#8' aGa0`4#3p1:1a: aaZ +v0`0:2Bzc2A:0X3= gcVXgcQ1KaKaVX8=a1v)Q1K`Ao3`Q]P<a4P<K=`QkSaaK%aL!zoaKbaLa4a1Q8@KaV3boa/oKaKa+aVa0@vc1qQ4vKaKaQqaVKaVaKaVVa0@v#oKqQ4v#V󀱐RaQr1Va0Da2aVaKaQqaa0@qV$Ka1v󀱐Sa2±Va0Da2Kava4a1aa0@v,v Qq4S a1aKa$ava4a1aa3aqQ3  Qq43 a1aKa$ava4a1aa3aqQ3  QP^43Va0D$vava4a1aa3aV QV@3a_,a1aKaV aVaKaa0H4,V Qg83KKa1aKa1aVaKaa0H(o Q2 KO3#a1qVa0Doaa4a1aa!av6a4Q2 aA13KbV!a1aKbv!ata4a1a`qaqKbV%atQ2 bV%a8_3KVa0DѸa4a1a`ea `IaQ2 Q43KcQa0DkbaKaa0H',5a  Q1A3Q4BX/ 1H/ /S+$+ q´0H#B3B20T#C21B20Xd;000?0+Bp+B+0Bpı3a0a0Pa0ȶQ03a0a0Pa0ȶQ030|V a0ȶ/b/ 3'a0a0X+ aCa' 3 $; V;a?(##Cd(aó+`3𐱑1cKkba)a1aob\4o1o kb_1o@3ְT aKasT2BrOZAz 3Zda az3:6a1A~rzqKaZa62q6qBsBq:2Bz^4^Q:1rBr@K('3ZOZ81Kc 5k=oQ1cVqQ1Oc`VKgcVX<a1OccVva<1cgcV5a6k5oa<=oo$VcbVcKc v5oaQ1aVaQ1Q2 5k 5g8<VPQQ8a0@ |VaqVOaVVaVVa'aVVKaaVᴶob1OKa|aq vVaQVV Q8GaQ a3r1VQ0HQVq_Kaq_KQ8VV a2 `Aav`GoaQqaVGVaV$/aQqaaaQqaKa!8aPGaVqV 3a1GaQ4vgK Q8VQV3V,+ Q0HQqa2K0L9o V aV `moa$V5ava3va1aaaa4a45qoOaQ2 9q ,+ Q4VK Q8Q2 $v8Q0PQ2 QQ20DQ8V a0@ `moa$a<Vaaa8a<a1aKa`EaV`q$Q8Q0PQ2 aQ6ooaQtQ2  QQ28Q0PQ2 QQ20@AQ8V a0DC6av6a2KaKcava0La1aaaa4a1OֱKaVKaP^,9aqQ0PQ2 oa_a8Q2  QQ28V4Q2 `saQVQ8V a0D(+aVaPkaQ2Kaav,Kaa8a<a1aKa+Voat`s(+Vo, Q<[agaQxQ2  QVaQ3, Q0HQgKa_$s;aQ8V a0HKaVaVvo avcvavo(oaa8a<a1aKa7va<g`gK'vo(a1Q2 vqQqV Q$34$1Q2 K$KKa42Kaaqo Vaao$avoavaOaVaV aVa1aaaa4avoaa1 aVOoV6o V Q1 aK-V Q488v Q0HQ8va11Q8vo!a11o%V a0P!a1v!a46oaaV6!Ka<aVaa8a<a1aKaoa1 boaQ1aVOaVa<aqQ2 v%% Q8v!8%% Q0HQ8%obVK!kb8bV 00:00:01,500 subtile subtile-0.3.2/fixtures/only_one.sup000066400000000000000000000050051474076126700174250ustar00rootroot00000000000000PGaZ  PGa  N$PGa;?K'    G /݀뀀e%Ognw, !"#J$%&7'(Ѐ)*+,-./01o234w5678W9Z:;<;=M>?@ABC䀀DۀEFvGHŀIJ̀KကLM]NOԀPQɀRSTU瀀VW~XYZ[`\]^_`abcdef1ghxij|klmnop_q׀rs@tuvkwxyz{|}~p{ۂcqŎڕP3ܛ+2z D>PGa0)N$NNNNNNNNN  !"#$%&'( )*"#%%+,-(.)*/011230 4 %%56 +7)* 10/8921  :;!<=>%%?@A,)*><B <%CDE'%+F%'G'HLq%%+,%' )ZNTZF85NqCnrs'%+,%' "#>VOtu%%+,%' )+H'9Xjvw 00:00:03,000 , subtile-0.3.2/fixtures/tiny.sub000066400000000000000000000040001474076126700165420ustar00rootroot00000000000000DĂ!! l4444444444444444444444h--u~q4444444444444444444444444h-ڀר44442l9subtile-0.3.2/src/000077500000000000000000000000001474076126700137705ustar00rootroot00000000000000subtile-0.3.2/src/content/000077500000000000000000000000001474076126700154425ustar00rootroot00000000000000subtile-0.3.2/src/content/area.rs000066400000000000000000000034731474076126700167270ustar00rootroot00000000000000use 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: usize::from(self.width()), h: usize::from(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.3.2/src/content/mod.rs000066400000000000000000000006451474076126700165740ustar00rootroot00000000000000//! 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.3.2/src/content/size.rs000066400000000000000000000002341474076126700167610ustar00rootroot00000000000000/// The dimensions of an image. #[derive(Debug)] pub struct Size { /// Width in pixels. pub w: usize, /// Height in pixels. pub h: usize, } subtile-0.3.2/src/errors.rs000066400000000000000000000010161474076126700156500ustar00rootroot00000000000000//! 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 SubtileError { /// 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.3.2/src/image/000077500000000000000000000000001474076126700150525ustar00rootroot00000000000000subtile-0.3.2/src/image/mod.rs000066400000000000000000000036761474076126700162130ustar00rootroot00000000000000//! Module for `Image` manipulation. mod pixels; mod utils; pub use pixels::{luma_a_to_luma, luma_a_to_luma_convertor}; pub use utils::{dump_images, DumpError}; use crate::content::Area; use image::{GrayImage, ImageBuffer, Luma, Pixel}; /// Define access to Size of an Image. Used for Subtitle content. pub trait ImageSize { /// access to width of the image fn width(&self) -> u32; /// access to height of the image fn height(&self) -> u32; } /// define access to Area of an Image. Used for Subtitle content. pub trait ImageArea { ///access to area of the image fn area(&self) -> Area; } // Implement ImageSize for all type than implement ImageArea impl ImageSize for U where U: ImageArea, { fn width(&self) -> u32 { u32::from(self.area().width()) } fn height(&self) -> u32 { u32::from(self.area().height()) } } /// define the behavior of generate a `ImageBuffer` from a `self` pub trait ToImage { /// Define the format of Sub-pixel of output type Pixel: Pixel; /// define the method to generate the image fn to_image(&self) -> ImageBuffer>; } /// Options for image generation. #[derive(Debug, Clone, Copy)] pub struct ToOcrImageOpt { /// Number of border pixels to add on the input image pub border: u32, /// Color of the text pub text_color: Luma, /// Color of the background pub background_color: Luma, } // Implement [`Default`] for [`ToOcrImageOpt`] with a border of 5 pixel // and colors black for text and white for background. impl Default for ToOcrImageOpt { fn default() -> Self { Self { border: 5, text_color: Luma([0]), background_color: Luma([255]), } } } /// Generate a `GrayImage` adapted for `OCR` from self. pub trait ToOcrImage { /// Generate the image for `OCR` in `GrayImage` format. fn image(&self, opt: &ToOcrImageOpt) -> GrayImage; } subtile-0.3.2/src/image/pixels.rs000066400000000000000000000036231474076126700167300ustar00rootroot00000000000000use image::{Luma, LumaA, Primitive}; use std::borrow::Borrow; /// Pixel convert function to remove alpha. /// Convert from [`LumaA`] to [`Luma`], and are useful to prepare image for `ocr`. /// If the alpha and luma value of the pixel is greater than or equal to threshold values, /// the output is [`Primitive::DEFAULT_MIN_VALUE`] (equivalent to black). /// Otherwise, the returned value is [`Primitive::DEFAULT_MAX_VALUE`] (equivalent to white). /// /// * `A`: alpha threshold /// * `L` : luma threshold /// /// # Panics /// Will panic if `P`(Primitive) is not initializable from value `L` and `A`. pub fn luma_a_to_luma(luma: In) -> Luma

where In: Borrow>, P: Primitive, { let luma = luma.borrow(); let luminance = luma[0]; //0 : Luminance idx let alpha = luma[1]; //1 : Alpha idx if alpha >= P::from(A).unwrap() && luminance >= P::from(L).unwrap() { Luma([P::DEFAULT_MIN_VALUE]) } else { Luma([P::DEFAULT_MAX_VALUE]) } } /// Create and return a closure than convert a Pixel from [`LumaA`] to [`Luma`] /// with apply threasold value from function parameters. If the alpha and luma value /// of the pixel is greater than or equal to threshold values, the output is [`Primitive::DEFAULT_MIN_VALUE`] (equivalent to black). /// Otherwise, the returned value is [`Primitive::DEFAULT_MAX_VALUE`] (equivalent to white). /// /// * `alpha_t`: alpha threshold /// * `luma_t`: luma threshold pub fn luma_a_to_luma_convertor(alpha_t: P, luma_t: P) -> impl Fn(In) -> Luma

where P: Primitive, In: Borrow>, { move |luma| { let luma = luma.borrow(); let luminance = luma[0]; //0 : Luminance idx let alpha = luma[1]; //1 : Alpha idx if alpha >= alpha_t && luminance >= luma_t { Luma([P::DEFAULT_MIN_VALUE]) } else { Luma([P::DEFAULT_MAX_VALUE]) } } } subtile-0.3.2/src/image/utils.rs000066400000000000000000000045401474076126700165630ustar00rootroot00000000000000use crate::SubtileError; use image::{EncodableLayout, Pixel, PixelWithColorType}; use std::{ borrow::Borrow, fs::create_dir_all, io, ops::Deref, path::{Path, PathBuf}, }; use thiserror::Error; /// 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. /// /// # Errors /// Will return `DumpError::Folder` if the output folder creation failed. /// Will return `DumpError::DumpImage` if the dump of one image failed. #[profiling::function] pub fn dump_images<'a, Iter, Img, P, Container>( path: &str, images: Iter, ) -> Result<(), SubtileError> where P: Pixel + PixelWithColorType + 'a, [P::Subpixel]: EncodableLayout, Container: Deref + 'a, Img: Borrow>, Iter: 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.borrow()).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.3.2/src/lib.rs000066400000000000000000000016761474076126700151160ustar00rootroot00000000000000//! `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 pgs; pub mod srt; pub mod time; mod util; pub mod vobsub; pub use errors::SubtileError; pub use pgs::SupParser; subtile-0.3.2/src/pgs/000077500000000000000000000000001474076126700145615ustar00rootroot00000000000000subtile-0.3.2/src/pgs/decoder.rs000066400000000000000000000112551474076126700165400ustar00rootroot00000000000000use crate::time::{TimePoint, TimeSpan}; use std::io::{BufRead, Seek}; use super::{ ods::{self, ObjectDefinitionSegment}, pds, pgs_image::RleEncodedImage, segment::{read_header, skip_segment, SegmentTypeCode}, PgsError, }; /// Trait of `Presentation Graphic Stream` decoding. pub trait PgsDecoder { /// Type of the Output data for the image. type Output; /// Parse next subtitle `PGS` and return an `Output` value. /// The `Output` depending of the data we want to decode. /// /// # Errors /// Return the error happened during parsing or decoding. fn parse_next(reader: &mut R) -> Result, PgsError> where R: BufRead + Seek; } /// Decoder for `PGS` who provide only the times of subtitles. pub struct DecodeTimeOnly; impl PgsDecoder for DecodeTimeOnly { type Output = TimeSpan; fn parse_next(reader: &mut R) -> Result, PgsError> where R: BufRead + Seek, { let mut start_time = None; let mut subtitle = None; while let Some(segment_header) = { if subtitle.is_some() { None } else { read_header(reader).transpose() } } { let seg_header = segment_header?; match seg_header.type_code() { SegmentTypeCode::End => { let time = TimePoint::from_msecs(i64::from(seg_header.presentation_time())); if let Some(start_time) = start_time { subtitle = Some(TimeSpan::new(start_time, time)) } else { start_time = Some(time); } } _ => { // Segment content are not taken into account, skipped skip_segment(reader, &seg_header)?; } } } Ok(subtitle) } } /// Decoder for `PGS` who provide the times and images of the subtitles. pub struct DecodeTimeImage {} impl PgsDecoder for DecodeTimeImage { type Output = (TimeSpan, RleEncodedImage); fn parse_next(reader: &mut R) -> Result, PgsError> where R: BufRead + Seek, { let mut start_time = None; let mut subtitle = None; let mut palette = None; let mut image = None; let mut prev_ods = None; while let Some(segment) = { if subtitle.is_some() { None } else { read_header(reader).transpose() } } { let header = segment?; match header.type_code() { SegmentTypeCode::Pds => { let seg_size = header.size() as usize; let pds = pds::read(reader, seg_size)?; palette = Some(pds.palette); } SegmentTypeCode::Ods => { let seg_size = header.size() as usize; let ods = ods::read(reader, seg_size, prev_ods.take())?; // If data are complete, construct `image` from palette and image data // otherwise, keep read data to complete it with data from following segment. if let ObjectDefinitionSegment::Complete(ods) = ods { let palette = palette.take().ok_or(PgsError::MissingPalette)?; image = Some(RleEncodedImage::new( ods.width, ods.height, palette, ods.object_data, )); } else { prev_ods = Some(ods); } } SegmentTypeCode::End => { let time = TimePoint::from_msecs(i64::from(header.presentation_time())); if let Some(start_time) = start_time { let times = TimeSpan::new(start_time, time); let image = image.take().ok_or(PgsError::MissingImage)?; subtitle = Some((times, image)); } else { start_time = Some(time); } } _ => { // Segment not taken into account are skipped skip_segment(reader, &header)?; } }; } assert!(palette.is_none()); // palette should be transferred into image before get out of the function. assert!(prev_ods.is_none()); // Ods data should be converted into image before get out of the function. Ok(subtitle) } } subtile-0.3.2/src/pgs/mod.rs000066400000000000000000000077051474076126700157170ustar00rootroot00000000000000//! Read functionalities for Presentation Graphic Stream (.sup) //! //! Presentation Graphic Stream (SUP files) `BluRay` Subtitle Format doc : //! //! mod decoder; mod ods; mod pds; mod pgs_image; mod segment; mod sup; mod u24; pub use decoder::{DecodeTimeImage, DecodeTimeOnly, PgsDecoder}; pub use pgs_image::{RleEncodedImage, RleToImage}; pub use sup::SupParser; use self::segment::SegmentTypeCode; use std::{ io::{self, BufRead, Seek}, path::PathBuf, }; use thiserror::Error; /// Error for `Pgs` handling. #[derive(Debug, Error)] pub enum PgsError { /// 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, }, /// Encapsulates errors from `Object Definition Segment` parsing. #[error("Object Definition Segment parsing")] ODSParse(#[from] ods::Error), /// Encapsulates errors from `Palette Definition Segment` parsing. #[error("Palette Definition Segment parsing")] PDSParse(#[from] pds::Error), /// Invalid segment type code value. #[error("Invalid value '{value:#02x}' for Segment Type Code ")] SegmentInvalidTypeCode { /// Value tried to be Interpréted in Segment Type. value: u8, }, /// An error occurred during Segment Header reading. #[error("Failed to read a complete segment header.")] SegmentFailReadHeader, /// Missing expected `PG` Magic number. #[error("Unable to read segment - PG missing!")] SegmentPGMissing, /// `ReadError` occurred during skipping the segment. #[error("Skipping Segment {type_code}")] SegmentSkip { /// Parent `ReadError` #[source] source: ReadError, /// type code of the segment we skip type_code: SegmentTypeCode, }, /// Error if image is missing to complete the parsing of a subtitle. #[error("Missing image during `Presentation Graphic Stream (PGS)` parsing")] MissingImage, /// Palette is missing after image parsing. #[error("Missing palette after image parsing")] MissingPalette, } /// Error from data read for parsing. #[derive(Debug, Error)] pub enum ReadError { /// Reading of the buffer have failed. #[error("Failed read buffer of size : {buffer_size}")] FailedReadBuffer { /// `io` error #[source] source: io::Error, /// size of the buffer buffer_size: usize, }, /// An error has occurred during buffer filling from reader. #[error("Failed to fill buffer from Reader")] FailedFillBuf(#[source] io::Error), /// An error has occurred during seek in reader. #[error("Seek failed")] FailedSeek(#[source] io::Error), } /// Super-trait of `BufRead` + `Seek` to extend reading functionalities useful for parsing. pub trait ReadExt where Self: BufRead + Seek, { /// Read a buffer from a reader with error management. /// /// # Errors /// /// Will return `FailedReadBuffer` if `read_exact` failed. fn read_buffer(&mut self, to_read: &mut [u8]) -> Result<(), ReadError> { self.read_exact(to_read) .map_err(|source| ReadError::FailedReadBuffer { source, buffer_size: to_read.len(), }) } /// Skip data from a reader with error management. /// /// # Errors /// /// Will return `FailedFillBuf` if `fill_buf` failed. /// `FailedSeek` if `seek` failed. fn skip_data(&mut self, to_skip: usize) -> Result<(), ReadError> { let buff = self.fill_buf().map_err(ReadError::FailedFillBuf)?; if buff.len() >= to_skip { self.consume(to_skip); } else { self.seek_relative(to_skip as i64) .map_err(ReadError::FailedSeek)?; } Ok(()) } } impl ReadExt for U where U: BufRead + Seek {} subtile-0.3.2/src/pgs/ods.rs000066400000000000000000000173441474076126700157250ustar00rootroot00000000000000use super::{u24::u24, ReadError, ReadExt}; use std::{ fmt::{Debug, Display}, io::{self, BufRead, Seek}, }; use thiserror::Error; /// Error `ODS` (Object Definition Segment) handling. #[derive(Debug, Error)] pub enum Error { /// Error while tried reading `LastInSequence` flag. #[error("Reading `LastInSequenceFlag` failed")] LastInSequenceFlagReadData(#[source] io::Error), /// Value read for `LastInSequence` flag is invalid. #[error("LastInSequenceFlag : '{value:02x}' is not a valid value")] LastInSequenceFlagInvalidValue { value: u8 }, /// Value of flag `LastInSequence` is not managed by the current code. #[error("LastInSequenceFlag::'{0}' flag is not mananged.")] LastInSequenceFlagNotManaged(LastInSequenceFlag), /// Failed during `Object ID` and `Object Version Number` skipping. #[error("Skipping `Object ID` and `Object Version Number`")] SkipObjectIdAndVerNum(#[source] ReadError), /// Failed during `Object Data Length` reading. #[error("Read `Object Data Length` field.")] ReadObjectDataLength(#[source] io::Error), /// Failed during read `Width` of the image. #[error("Read With of the image incarried by the `Object Definition Segment`(s)")] ReadWidth(#[source] io::Error), /// Failed during read `Height` of the image. #[error("Read Height of the image incarried by the `Object Definition Segment`(s)")] ReadHeight(#[source] io::Error), /// The read of object data failed. #[error("Try reading object data (buffer slice size: {buff_size})")] ObjectData { #[source] source: io::Error, buff_size: usize, }, } #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum LastInSequenceFlag { Last = 0x40, First = 0x80, FirstAndLast = 0xC0, } impl TryFrom for LastInSequenceFlag { type Error = Error; fn try_from(value: u8) -> Result { match value { 0x40 => Ok(Self::Last), 0x80 => Ok(Self::First), 0xC0 => Ok(Self::FirstAndLast), value => Err(Error::LastInSequenceFlagInvalidValue { value }), } } } impl From for u8 { fn from(val: LastInSequenceFlag) -> Self { val as Self } } impl From for &'static str { fn from(val: LastInSequenceFlag) -> Self { match val { LastInSequenceFlag::Last => "Last", LastInSequenceFlag::First => "First", LastInSequenceFlag::FirstAndLast => "First and last", } } } impl Debug for LastInSequenceFlag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let hex: u8 = (*self).into(); write!(f, "{hex:#02x}-{self}") } } impl Display for LastInSequenceFlag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let friendly: &str = (*self).into(); write!(f, "{friendly} in sequence") } } impl LastInSequenceFlag { fn read(reader: &mut Reader) -> Result { let mut last_in_sequence_byte = [0]; reader .read_exact(&mut last_in_sequence_byte) .map_err(Error::LastInSequenceFlagReadData)?; Self::try_from(last_in_sequence_byte[0]) } } #[derive(Debug)] pub enum ObjectDefinitionSegment { Partial { data: ObjectDefinitionSegmentData, amount_of_data_read: usize, }, Complete(ObjectDefinitionSegmentData), } /// This segment defines the graphics object : it contain the image. /// The `object_data` contain theimage data compressed using Run-length Encoding (RLE) #[derive(Debug)] //TODO: define a custom Debug pub struct ObjectDefinitionSegmentData { pub width: u16, pub height: u16, pub object_data: Vec, } pub fn read( reader: &mut Reader, segments_size: usize, current_ods: Option, ) -> Result { handle_object_fields(reader)?; let last_in_sequence_flag = LastInSequenceFlag::read(reader)?; match current_ods { None => { assert!( last_in_sequence_flag == LastInSequenceFlag::First || last_in_sequence_flag == LastInSequenceFlag::FirstAndLast ); let data_size = read_obj_data_length(reader)?; let (width, height) = read_img_size(reader)?; let data_size = data_size - 4; // don't know why for now !!! Object Data Length include Width + Height ? let mut object_data = vec![0; data_size]; // Create a `Vec` for contain data of object (image) let read_data_size = segments_size - 11; // Only read data from this segment, additional data are in the next segment, if there are any. let data_buff = &mut object_data.as_mut_slice()[0..read_data_size]; read_object_data(reader, data_buff)?; let data = ObjectDefinitionSegmentData { width, height, object_data, }; if last_in_sequence_flag == LastInSequenceFlag::FirstAndLast { assert!(read_data_size == data_size); assert!(segments_size == 11 + data_size); Ok(ObjectDefinitionSegment::Complete(data)) } else if last_in_sequence_flag == LastInSequenceFlag::First { Ok(ObjectDefinitionSegment::Partial { data, amount_of_data_read: read_data_size, }) } else { Err(Error::LastInSequenceFlagNotManaged(last_in_sequence_flag)) } } Some(ObjectDefinitionSegment::Partial { mut data, amount_of_data_read, }) => { assert!(last_in_sequence_flag == LastInSequenceFlag::Last); //TODO: not first and not last ? let start_idx = amount_of_data_read; let end_idx = start_idx + (segments_size - 4); let read_slice = &mut data.object_data.as_mut_slice()[start_idx..end_idx]; read_object_data(reader, read_slice)?; Ok(ObjectDefinitionSegment::Complete(data)) } Some(ObjectDefinitionSegment::Complete(_)) => { panic!("read shouln'd be called with a `Complete` `ObjectDefinitionSegment`"); } } } // Handle `Object ID` and `Object Version Number` fields by skip it. // They are not useful for current subtitle management. fn handle_object_fields(reader: &mut Reader) -> Result<(), Error> { reader .skip_data(2 + 1) .map_err(Error::SkipObjectIdAndVerNum)?; Ok(()) } // Read the `Object Data Length` field and return value in `usize`. fn read_obj_data_length(reader: &mut Reader) -> Result { let mut buffer = [0; 3]; reader .read_exact(&mut buffer) .map_err(Error::ReadObjectDataLength)?; let object_data_length = u24::from(<&[u8] as TryInto<[u8; 3]>>::try_into(&buffer).unwrap()); Ok(object_data_length.to_u32().try_into().unwrap()) } // Read the image size (width and height) fields. fn read_img_size(reader: &mut Reader) -> Result<(u16, u16), Error> { let mut buffer = [0; 2]; reader.read_exact(&mut buffer).map_err(Error::ReadWidth)?; let width = u16::from_be_bytes(buffer); reader.read_exact(&mut buffer).map_err(Error::ReadHeight)?; let height = u16::from_be_bytes(buffer); Ok((width, height)) } // Read the `Object data` field. fn read_object_data( reader: &mut Reader, data_buff: &mut [u8], ) -> Result<(), Error> { reader .read_exact(data_buff) .map_err(|source| Error::ObjectData { source, buff_size: data_buff.len(), }) } subtile-0.3.2/src/pgs/pds.rs000066400000000000000000000051521474076126700157200ustar00rootroot00000000000000use std::io::{self, Read}; use thiserror::Error; /// Error `PDS` (Palette Definition Segment) handling. #[derive(Debug, Error)] pub enum Error { /// Read `PaletteDefinitionSegment` in a buffer failed. #[error("Failed to read buffer with `PaletteDefinitionSegment`")] BufferParse(#[source] io::Error), } #[derive(Debug, Clone)] pub struct Palette { entries: Vec, offset: i16, } impl Palette { fn new(entries: Vec) -> Self { let offset = compute_offset(&entries); Self { entries, offset } } pub fn get(&self, id: u8) -> Option<&PaletteEntry> { let idx = i16::from(id) + self.offset; self.entries.get(idx as usize) } } fn compute_offset(palette: &[PaletteEntry]) -> i16 { //HACK offset is computed only on the first element, should be checked for all entries if palette.is_empty() { 0 } else { 0 - i16::from(palette[0]._palette_entry_id) } } #[derive(Debug, Clone)] pub struct PaletteEntry { _palette_entry_id: u8, // Entry number of the palette pub luminance: u8, // Luminance (Y value) _color_difference_red: u8, // Color Difference Red (Cr value) _color_difference_blue: u8, // Color Difference Blue (Cb value) pub transparency: u8, // Transparency (Alpha value) } #[derive(Debug)] pub(crate) struct PaletteDefinitionSegment { _palette_id: u8, // ID of the palette _palette_version_number: u8, // Version of this palette within the Epoch pub palette: Palette, } pub(crate) fn read( reader: &mut R, segments_size: usize, ) -> Result { let mut pds_buf = vec![0; segments_size]; reader .read_exact(&mut pds_buf) .map_err(Error::BufferParse)?; let palette_id = pds_buf[0]; let palette_version_number = pds_buf[1]; let nb_palette_entry: usize = (segments_size - 2) / 5; assert_eq!((nb_palette_entry * 5) + 2, segments_size); let range = 0..nb_palette_entry; let palette_entries = range .map(|idx| { let offset = 2 + (idx * 5); PaletteEntry { _palette_entry_id: pds_buf[offset], luminance: pds_buf[offset + 1], _color_difference_red: pds_buf[offset + 2], _color_difference_blue: pds_buf[offset + 3], transparency: pds_buf[offset + 4], } }) .collect(); Ok(PaletteDefinitionSegment { _palette_id: palette_id, _palette_version_number: palette_version_number, palette: Palette::new(palette_entries), }) } subtile-0.3.2/src/pgs/pgs_image.rs000066400000000000000000000216041474076126700170650ustar00rootroot00000000000000use super::pds::{Palette, PaletteEntry}; use crate::image::{ImageSize, ToImage, ToOcrImage, ToOcrImageOpt}; use image::{ImageBuffer, Luma, LumaA, Pixel, Primitive}; use std::io::{ErrorKind, Read}; /// Define a type of `fn` who covert pixel from `PaletteEntry` to a target color type. type PixelConversion = fn(&PaletteEntry) -> TargetColor; /// Store Image data directly from `PGS`. #[derive(Clone)] pub struct RleEncodedImage { width: u16, height: u16, palette: Palette, raw: Vec, } impl RleEncodedImage { /// Create a `RleEncodedImage` from [`SupParser`] /// /// [`SupParser`]: super::sup::SupParser #[must_use] pub const fn new(width: u16, height: u16, palette: Palette, raw: Vec) -> Self { Self { width, height, palette, raw, } } /// Iterate on image pixels converted with a specified function. pub fn pixels( &self, convert: PixelConversion>, ) -> RlePixelIterator> { RlePixelIterator { rle_image: self, raw_data: &self.raw, current_color: LumaA([D::DEFAULT_MIN_VALUE, D::DEFAULT_MAX_VALUE]), default_color: LumaA([D::DEFAULT_MAX_VALUE, D::DEFAULT_MIN_VALUE]), // Default: white + transparent nb_remaining_pixels: 0, convert, } } } impl ImageSize for RleEncodedImage { fn width(&self) -> u32 { u32::from(self.width) } fn height(&self) -> u32 { u32::from(self.height) } } /// Create an iterator over [`RleEncodedImage`] pixels. impl<'a> IntoIterator for &'a RleEncodedImage { type Item = LumaA; type IntoIter = RlePixelIterator<'a, LumaA>; fn into_iter(self) -> Self::IntoIter { RlePixelIterator { rle_image: self, raw_data: &self.raw, current_color: LumaA([ ::DEFAULT_MIN_VALUE, ::DEFAULT_MAX_VALUE, ]), // setup to luma min (black), alpha max (opaque) default_color: LumaA([ ::DEFAULT_MAX_VALUE, ::DEFAULT_MIN_VALUE, ]), // Default: white + transparent nb_remaining_pixels: 0, convert: pe_to_luma_a, } } } /// Convert a [`PaletteEntry`] to a `LumaA`

fn pe_to_luma_a(input: &PaletteEntry) -> LumaA

{ let luminance = P::from(input.luminance).unwrap(); let alpha = P::from(input.transparency).unwrap(); LumaA([luminance, alpha]) } /// This struct implement [`ToImage`] to generate an `ImageBuffer` from /// a [`RleEncodedImage`] and a pixel conversion function. pub struct RleToImage<'a, P, C> where P: Pixel, C: Fn(LumaA) -> P, { rle_image: &'a RleEncodedImage, conv_fn: C, } impl<'a, P, C> RleToImage<'a, P, C> where P: Pixel, C: Fn(LumaA) -> P, { /// Create a struct to generate an image from [`RleEncodedImage`] pub const fn new(rle_image: &'a RleEncodedImage, conv_fn: C) -> Self { Self { rle_image, conv_fn } } } impl ToImage for RleToImage<'_, P, C> where P: Pixel, C: Fn(LumaA) -> P, { type Pixel = P; #[profiling::function] fn to_image(&self) -> ImageBuffer> where P: Pixel, { let width = self.rle_image.width(); let height = self.rle_image.height(); let pixel_iter = self.rle_image.into_iter(); let buf_size = (width * height) as usize * P::CHANNEL_COUNT as usize; let mut buf = Vec::with_capacity(buf_size); pixel_iter .map(|p| (self.conv_fn)(p)) .for_each(|p| buf.extend_from_slice(p.channels())); ImageBuffer::>::from_vec(width, height, buf) .expect("Failed to create image buffer") } } /// Implement [`ToOcrImage`] from [`RleEncodedImage`] impl ToOcrImage for RleToImage<'_, Luma, C> where C: Fn(LumaA) -> Luma, { #[profiling::function] fn image(&self, opt: &ToOcrImageOpt) -> image::GrayImage { let width = self.rle_image.width(); let height = self.rle_image.height(); let border = opt.border; let raw_pixels = self.rle_image.into_iter().collect::>(); ImageBuffer::from_fn(width + border * 2, height + border * 2, |x, y| { if x < border || x >= width + border || y < border || y >= height + border { opt.background_color } else { let offset = (y - border) * width + (x - border); let pixel = raw_pixels[offset as usize]; (self.conv_fn)(pixel) } }) } } /// struct to iterate on pixel of an `Rle` image. pub struct RlePixelIterator<'a, C> { rle_image: &'a RleEncodedImage, raw_data: &'a [u8], current_color: C, default_color: C, nb_remaining_pixels: u16, convert: PixelConversion, } /// Allow iterate over pixels of image encoded in `Rle`. impl Iterator for RlePixelIterator<'_, Pix> where Sub: Primitive, Pix: Copy + Pixel, { type Item = Pix; fn next(&mut self) -> Option { if self.nb_remaining_pixels > 0 { self.nb_remaining_pixels -= 1; Some(self.current_color) } else if let Some((color_id, nb_pixel)) = self.read_next_pixel() { let color = if let Some(color) = self.rle_image.palette.get(color_id) { (self.convert)(color) } else { // If color_id is not present in palette, return default value self.default_color }; self.current_color = color; self.nb_remaining_pixels = nb_pixel - 1; Some(self.current_color) } else { None // End of pixels } } fn size_hint(&self) -> (usize, Option) { let nb_pixels = (self.rle_image.width() * self.rle_image.height()) as usize; (nb_pixels, Some(nb_pixels)) } } impl ExactSizeIterator for RlePixelIterator<'_, Pix> where Sub: Primitive, Pix: Copy + Pixel, { } impl RlePixelIterator<'_, C> { /// Read next pixel info(color and number of instance). fn read_next_pixel(&mut self) -> Option<(u8 /*color */, u16 /*nb_pixels*/)> { const MARKER: u8 = 0; const COLOR_0: u8 = 0; loop { let mut color: [u8; 1] = [0; 1]; let res = self.raw_data.read_exact(&mut color); if let Err(err) = res { if err.kind() == ErrorKind::UnexpectedEof { return None; } } let next = color[0]; if next == MARKER { let mut l2 = [0; 1]; self.raw_data.read_exact(&mut l2).unwrap(); if l2[0] == MARKER { //break; // End of line } else { let byte = l2[0]; let nb_pixels = match CountMarker::from(byte) { CountMarker::Long => { let mut l3 = [0; 1]; self.raw_data.read_exact(&mut l3).unwrap(); let count_bytes = [byte & 0b0011_1111, l3[0]]; u16::from_be_bytes(count_bytes) } CountMarker::Short => { let count_bits = byte & 0b0011_1111; u16::from(u8::from_be(count_bits)) } }; let color_marker = ColorMarker::from(byte); let color = match color_marker { ColorMarker::Color0 => COLOR_0, ColorMarker::ColorN => { let mut color = [0; 1]; self.raw_data.read_exact(&mut color).unwrap(); color[0] } }; return Some((color, nb_pixels)); } } else { return Some((next, 1)); } } } } /// Decode the color marker. enum ColorMarker { /// color 0 : black Color0, /// color N : color define in code ColorN, } impl From for ColorMarker { fn from(value: u8) -> Self { if (value & 0b1000_0000) > 0 { Self::ColorN } else { Self::Color0 } } } /// Decode the pixels count marcker. enum CountMarker { /// the number of pixels is between 1 and 63 Short, /// the number of pixels is between 64 and 16383 Long, } impl From for CountMarker { fn from(value: u8) -> Self { if (value & 0b0100_0000) > 0 { Self::Long } else { Self::Short } } } subtile-0.3.2/src/pgs/segment.rs000066400000000000000000000066641474076126700166050ustar00rootroot00000000000000use super::{PgsError, ReadExt}; use std::{ fmt, io::{BufRead, ErrorKind, Seek}, }; // Segment start Magic Number const MAGIC_NUMBER: [u8; 2] = [0x50, 0x47]; #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum SegmentTypeCode { Pds = 0x14, Ods = 0x15, Pcs = 0x16, Wds = 0x17, End = 0x80, } impl TryFrom for SegmentTypeCode { type Error = PgsError; fn try_from(value: u8) -> Result { match value { 0x14 => Ok(Self::Pds), 0x15 => Ok(Self::Ods), 0x16 => Ok(Self::Pcs), 0x17 => Ok(Self::Wds), 0x80 => Ok(Self::End), _ => Err(PgsError::SegmentInvalidTypeCode { value }), } } } impl From for u8 { fn from(val: SegmentTypeCode) -> Self { val as Self } } impl From for &'static str { fn from(val: SegmentTypeCode) -> Self { match val { SegmentTypeCode::Pds => "PDS", SegmentTypeCode::Ods => "ODS", SegmentTypeCode::Pcs => "PCS", SegmentTypeCode::Wds => "WDS", SegmentTypeCode::End => "END", } } } impl fmt::Debug for SegmentTypeCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let hex: u8 = (*self).into(); write!(f, "{hex:#02x}-{self}") } } impl fmt::Display for SegmentTypeCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let friendly: &str = (*self).into(); write!(f, "{friendly}") } } /// Struct of segment header. #[derive(Debug)] pub(crate) struct SegmentHeader { /// Presentation Timestamp. pts: u32, /// Code of the Segment Type type_code: SegmentTypeCode, /// Size of the segment. size: u16, } impl SegmentHeader { pub const fn presentation_time(&self) -> u32 { self.pts / 90 // Return time in milliseconds } pub const fn type_code(&self) -> SegmentTypeCode { self.type_code } pub const fn size(&self) -> u16 { self.size } } /// Length of the segment Header const HEADER_LEN: usize = 2 + 4 + 4 + 1 + 2; /// Read the segment header pub fn read_header(reader: &mut R) -> Result, PgsError> { let mut buffer = [0u8; HEADER_LEN]; match reader.read_exact(&mut buffer) { Ok(()) => parse_segment_header(buffer), Err(err) if err.kind() == ErrorKind::UnexpectedEof => { // Buffer is empty, just return to end parsing Ok(None) } Err(err) => { println!("{err:?}"); Err(PgsError::SegmentFailReadHeader) } } } fn parse_segment_header(buffer: [u8; HEADER_LEN]) -> Result, PgsError> { if buffer[0..2] != MAGIC_NUMBER { return Err(PgsError::SegmentPGMissing); } let pts = u32::from_be_bytes(buffer[2..6].try_into().unwrap()); let type_code = SegmentTypeCode::try_from(buffer[10])?; let size = u16::from_be_bytes(buffer[11..13].try_into().unwrap()); Ok(Some(SegmentHeader { pts, type_code, size, })) } /// skip segment pub fn skip_segment( reader: &mut R, header: &SegmentHeader, ) -> Result<(), PgsError> { let data_size: usize = header.size() as usize; reader .skip_data(data_size) .map_err(|source| PgsError::SegmentSkip { source, type_code: header.type_code(), }) } subtile-0.3.2/src/pgs/sup.rs000066400000000000000000000044751474076126700157500ustar00rootroot00000000000000use super::{PgsDecoder, PgsError}; use std::{ fs::{self, File}, io::{BufRead, BufReader, Seek}, marker::PhantomData, path::Path, }; /// To parse `Presentation Graphic Stream` content `BluRay` subtitle format (`.sup` file). pub struct SupParser where Reader: BufRead, Decoder: PgsDecoder, { reader: Reader, phantom_data: PhantomData, } impl SupParser where Reader: BufRead + Seek, Decoder: PgsDecoder, { /// create a parser of from a buffered reader (impl [`std::io::BufRead`] trait). pub const fn new(reader: Reader) -> Self { Self { reader, phantom_data: PhantomData, } } /// Create a parser for a `*.sup` file from the path of the file. #[profiling::function] pub fn from_file

(path: P) -> Result, Decoder>, PgsError> where P: AsRef, { let path = path.as_ref(); let sup_file = fs::File::open(path).map_err(|source| PgsError::Io { source, path: path.into(), })?; let reader = BufReader::new(sup_file); Ok(SupParser::new(reader)) } } impl Iterator for SupParser where Reader: BufRead + Seek, Decoder: PgsDecoder, { type Item = Result; fn next(&mut self) -> Option { Decoder::parse_next(&mut self.reader).transpose() } // Set lower bound to promote the allocation of a minimum number of elements. fn size_hint(&self) -> (usize, Option) { (500, None) } } #[cfg(test)] mod tests { use super::SupParser; use crate::{ pgs::DecodeTimeOnly, time::{TimePoint, TimeSpan}, }; use std::{fs::File, io::BufReader}; #[test] fn parse_only_one_sub() { let controls = [TimeSpan::new( TimePoint::from_msecs(500), TimePoint::from_msecs(1499), )]; let parser = SupParser::, DecodeTimeOnly>::from_file("./fixtures/only_one.sup") .unwrap(); let file_subtitles = parser.map(|sub| sub.unwrap()).collect::>(); assert!(file_subtitles.iter().eq(controls.iter())); assert!(file_subtitles.len() == 1); } } subtile-0.3.2/src/pgs/u24.rs000066400000000000000000000010111474076126700155320ustar00rootroot00000000000000use std::fmt::Debug; #[derive(Copy, Clone)] #[allow(non_camel_case_types)] #[repr(transparent)] pub struct u24([u8; 3]); impl u24 { pub const fn to_u32(self) -> u32 { let Self([a, b, c]) = self; u32::from_be_bytes([0, a, b, c]) } } impl From<[u8; 3]> for u24 { fn from(value: [u8; 3]) -> Self { Self(value) } } impl Debug for u24 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = self.to_u32(); write!(f, "{value}") } } subtile-0.3.2/src/srt.rs000066400000000000000000000014361474076126700151520ustar00rootroot00000000000000//! 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.3.2/src/time/000077500000000000000000000000001474076126700147265ustar00rootroot00000000000000subtile-0.3.2/src/time/mod.rs000066400000000000000000000001711474076126700160520ustar00rootroot00000000000000//! Subtitle Time management mod time_point; mod time_span; pub use time_point::TimePoint; pub use time_span::TimeSpan; subtile-0.3.2/src/time/time_point.rs000066400000000000000000000052731474076126700174520ustar00rootroot00000000000000use core::fmt; use std::ops::Neg; /// Define a time in milliseconds #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct TimePoint(i64); impl TimePoint { /// Create a `TimePoint` from milliseconds #[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_much_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.3.2/src/time/time_span.rs000066400000000000000000000037161474076126700172620ustar00rootroot00000000000000use super::TimePoint; use core::fmt::{self, Debug}; /// Define a time span with a start time and an end time. #[derive(Clone, Copy, Default, 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.3.2/src/util.rs000066400000000000000000000010241474076126700153100ustar00rootroot00000000000000//! 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 fmt::Debug for BytesFormatter<'_> { 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.3.2/src/vobsub/000077500000000000000000000000001474076126700152705ustar00rootroot00000000000000subtile-0.3.2/src/vobsub/decoder.rs000066400000000000000000000033061474076126700172450ustar00rootroot00000000000000use super::{img::VobSubRleImage, VobSubIndexedImage}; use crate::time::{TimePoint, TimeSpan}; /// 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; /// The trait `VobSubDecoder` define the behavior to output data from `VobSub` parsing. /// This trait is used by [`VobsubParser`] to allow various decoding of parsing data. /// /// [`VobSubParser`]: crate::vobsub::sub::VobsubParser pub trait VobSubDecoder<'a> { type Output; fn from_data( start_time: f64, end_time: Option, force: bool, image: VobSubRleImage<'a>, ) -> Self::Output; } /// Implement creation of a tuple of [`TimeSpan`] and [`VobSubIndexedImage`] from parsing. impl<'a> VobSubDecoder<'a> for (TimeSpan, VobSubIndexedImage) { type Output = Self; fn from_data( start_time: f64, end_time: Option, _force: bool, rle_image: VobSubRleImage<'a>, ) -> Self::Output { ( TimeSpan::new( TimePoint::from_secs(start_time), TimePoint::from_secs(end_time.unwrap_or(DEFAULT_SUBTITLE_LENGTH)), ), VobSubIndexedImage::from(rle_image), ) } } /// Decode data from `VobsubParser` and get only the [`TimeSpan`]. impl<'a> VobSubDecoder<'a> for TimeSpan { type Output = Self; fn from_data( start_time: f64, end_time: Option, _force: bool, _rle_image: VobSubRleImage<'a>, ) -> Self::Output { Self::new( TimePoint::from_secs(start_time), TimePoint::from_secs(end_time.unwrap_or(DEFAULT_SUBTITLE_LENGTH)), ) } } subtile-0.3.2/src/vobsub/idx.rs000066400000000000000000000102131474076126700164170ustar00rootroot00000000000000//! 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 crate::vobsub::IResultExt; use super::{ palette::{palette, DEFAULT_PALETTE}, sub, 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, } const PALETTE_KEY: &str = "palette"; impl Index { /// Open an `*.idx` file and the associated `*.sub` file. /// /// # Errors /// Will return VobSubError::Io if failed to open of read `.idx` or ``.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).or_else(|err| { if let VobSubError::MissingKey(PALETTE_KEY) = err { Ok(DEFAULT_PALETTE) } else { Err(err) } })?; 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 const 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::VobsubParser { sub::VobsubParser::new(&self.sub_data) } } /// Read the palette in `*.idx` file content /// /// # Errors /// Will return `VobSubError::MissingKey` if the palette key/value is not present /// Will return `VobSubError::PaletteError` if failed to read and parse palette value. /// /// # Panics /// Panic if the Regex creation failed #[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_KEY => { palette_val = Some( palette(val.as_bytes()) .to_result_no_rest() .map_err(VobSubError::PaletteError)?, ); } _ => trace!("Unimplemented idx key: {}", key), } } buf.clear(); } palette_val.ok_or(VobSubError::MissingKey(PALETTE_KEY)) } #[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.3.2/src/vobsub/img.rs000066400000000000000000000303671474076126700164230ustar00rootroot00000000000000//! Run-length encoded image format for subtitles. use core::fmt::{self, Debug}; use image::{ImageBuffer, Luma, Pixel, Rgb, Rgba}; use iter_fixed::IntoIteratorFixed; 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, NomError, VobSubError}; use crate::{ content::{Area, Size}, image::{ImageArea, ImageSize, ToImage, ToOcrImage, ToOcrImageOpt}, util::BytesFormatter, }; /// Errors of `vobsub` image 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 parsing error. #[error("Parsing scan line failed")] ScanLineParsing(#[source] NomError), } pub struct VobSubRleImage<'a> { area: Area, palette: [u8; 4], alpha: [u8; 4], image_data: VobSubRleImageData<'a>, } impl<'a> VobSubRleImage<'a> { pub const fn new( area: Area, palette: [u8; 4], alpha: [u8; 4], image_data: VobSubRleImageData<'a>, ) -> Self { Self { area, palette, alpha, image_data, } } pub fn size(&self) -> Size { self.area.size() } pub const fn palette(&self) -> &[u8; 4] { &self.palette } pub const fn alpha(&self) -> &[u8; 4] { &self.alpha } pub const fn raw_data(&self) -> &VobSubRleImageData<'a> { &self.image_data } } impl ImageArea for VobSubRleImage<'_> { fn area(&self) -> Area { self.area } } /// Handle `VobSub` `Rle` image data in one struct. pub struct VobSubRleImageData<'a> { data: [&'a [u8]; 2], } impl<'a> VobSubRleImageData<'a> { pub fn new(raw_data: &'a [u8], rle_offsets: [u16; 2], end: usize) -> Result { // 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 = usize::from(rle_offsets[0]); let start_1 = usize::from(rle_offsets[1]); if start_0 > start_1 || start_1 > end { Err(VobSubError::InvalidScanLineOffsets { start_0, start_1, end, }) } else { Ok(Self { data: [&raw_data[start_0..end], &raw_data[start_1..end]], }) } } } /// 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 { usize::from(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: &VobSubRleImageData) -> Result, Error> { trace!( "decompressing image {:?}, max: [0x{:x}, 0x{:x}]", &size, data.data[0].len(), data.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.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) } /// Manage image data from `VobSub` file. #[derive(Clone, PartialEq, Eq)] pub struct VobSubIndexedImage { /// 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. //TODO: encapsulate in dedicated type for avoiding error with palette 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 VobSubIndexedImage { /// Create a new `VobSubImage` #[must_use] pub const fn new(area: Area, palette: [u8; 4], alpha: [u8; 4], raw_image: Vec) -> Self { Self { area, palette, alpha, raw_image, } } /// Access to palette data #[must_use] pub const fn palette(&self) -> &[u8; 4] { &self.palette } /// Access to alpha data #[must_use] pub const fn alpha(&self) -> &[u8; 4] { &self.alpha } /// Access to pixel raw data of the image #[must_use] pub fn raw_image(&self) -> &[u8] { self.raw_image.as_slice() } } impl fmt::Debug for VobSubIndexedImage { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.debug_struct("VobSub Image") .field("area", &self.area) .field("palette", &self.palette) .field("alpha", &self.alpha) .finish_non_exhaustive() } } impl ImageArea for VobSubIndexedImage { fn area(&self) -> Area { self.area } } impl From> for VobSubIndexedImage { fn from(rle_image: VobSubRleImage) -> Self { let decompressed_image = decompress(rle_image.size(), rle_image.raw_data()).unwrap(); Self::new( rle_image.area(), *rle_image.palette(), *rle_image.alpha(), decompressed_image, ) } } /// convert rbg + alpha to `Rgba` #[must_use] pub fn conv_to_rgba(color: Rgb, alpha: u8) -> Rgba { Rgba([ color.channels()[0], color.channels()[1], color.channels()[2], alpha, ]) } /// This struct implement [`ToImage`] to generate an `ImageBuffer` from /// a [`VobSubIndexedImage`], a palette and a pixel conversion function. pub struct VobSubToImage<'a, I, P> where P: Pixel, { indexed_img: &'a VobSubIndexedImage, palette: &'a [I; 16], conv_fn: fn(I, u8) -> P, } impl<'a, I, P> VobSubToImage<'a, I, P> where P: Pixel, { /// Create a `VobSub` image converter from a [`VobSubIndexedImage`], a `palette` and /// a pixel conversion function. #[must_use] pub fn new(img: &'a VobSubIndexedImage, palette: &'a [I; 16], conv_fn: fn(I, u8) -> P) -> Self { Self { indexed_img: img, palette, conv_fn, } } fn compute_palette_color(&self, conv: fn(I, u8) -> P) -> [P; 4] where I: Clone, P: Pixel, { self.indexed_img .palette() .into_iter_fixed() .zip(self.indexed_img.alpha()) .map(|(&palette_idx, &alpha)| (self.palette[palette_idx as usize].clone(), alpha)) .map(|(luminance, alpha)| conv(luminance, alpha)) .collect() } } impl ToImage for VobSubToImage<'_, I, P> where I: Clone, P: Pixel, { type Pixel = P; #[profiling::function] fn to_image(&self) -> ImageBuffer> where P: Pixel, { let width = self.indexed_img.width(); let height = self.indexed_img.height(); let out_color_palette = self.compute_palette_color(self.conv_fn); let image = ImageBuffer::from_fn(width, height, |x, y| { let offset = y * width + x; let sub_palette_idx = self.indexed_img.raw_image()[offset as usize] as usize; out_color_palette[sub_palette_idx] }); image } } /// A struct to convert [`VobSubIndexedImage`] to image for `OCR` pub struct VobSubOcrImage<'a> { indexed_img: &'a VobSubIndexedImage, palette: &'a [f32; 16], } impl<'a> VobSubOcrImage<'a> { /// create the image converter. #[must_use] pub const fn new(indexed_img: &'a VobSubIndexedImage, palette: &'a [f32; 16]) -> Self { Self { indexed_img, palette, } } // Compute the output palette color fn compute_palette_color(&self, opt: ToOcrImageOpt) -> [Luma; 4] { self.indexed_img .palette() .into_iter_fixed() .zip(self.indexed_img.alpha()) .map(|(&palette_idx, &alpha)| (self.palette[palette_idx as usize], alpha)) .map(|(luminance, alpha)| { if alpha > 0 && luminance > 0. { opt.text_color } else { opt.background_color } }) .collect() } } impl ToOcrImage for VobSubOcrImage<'_> { #[profiling::function] fn image(&self, opt: &ToOcrImageOpt) -> image::GrayImage { let width = self.indexed_img.width(); let height = self.indexed_img.height(); let border = opt.border; let out_color_palette = self.compute_palette_color(*opt); let image = ImageBuffer::from_fn(width + border * 2, height + border * 2, |x, y| { if x < border || x >= width + border || y < border || y >= height + border { opt.background_color } else { let offset = (y - border) * width + (x - border); let sub_palette_idx = self.indexed_img.raw_image()[offset as usize] as usize; out_color_palette[sub_palette_idx] } }); image } } subtile-0.3.2/src/vobsub/mod.rs000066400000000000000000000204141474076126700164160ustar00rootroot00000000000000//! 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; //! //! use crate::subtile::{ //! image::{ImageSize, ImageArea, ToImage}, //! time::TimeSpan, //! vobsub::{conv_to_rgba, VobSubIndexedImage, VobSubToImage}, //! }; //! //! let idx = subtile::vobsub::Index::open("./fixtures/example.idx").unwrap(); //! for sub in idx.subtitles::<(TimeSpan, VobSubIndexedImage)>() { //! let (time_span, image) = sub.unwrap(); //! println!("Time: {:0.3}-{:0.3}", time_span.start, time_span.end); //! //println!("Always show: {:?}", sub.force()); //! let area = image.area(); //! println!("At: {}, {}", area.left(), area.top()); //! println!("Size: {}x{}", image.width(), image.height()); //! let img: image::RgbaImage = VobSubToImage::new(&image, idx.palette(), conv_to_rgba).to_image(); //! //! // 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 decoder; mod idx; mod img; mod mpeg2; mod palette; mod probe; mod sub; pub use self::{ idx::{read_palette, Index}, img::{conv_to_rgba, VobSubIndexedImage, VobSubOcrImage, VobSubToImage}, palette::{palette, Palette}, probe::{is_idx_file, is_sub_file}, sub::ErrorMissing, }; 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 parsing 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 incomplete #[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 occurred during parsing #[error("Error from nom : {0}")] Error(String), /// An Failure occurred 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.3.2/src/vobsub/mpeg2/000077500000000000000000000000001474076126700163025ustar00rootroot00000000000000subtile-0.3.2/src/vobsub/mpeg2/clock.rs000066400000000000000000000053731474076126700177530ustar00rootroot00000000000000use nom::{ bits::complete::{tag, take}, sequence::Tuple, IResult, }; use std::fmt; /// This represents the 90 kHz, 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.3.2/src/vobsub/mpeg2/mod.rs000066400000000000000000000002651474076126700174320ustar00rootroot00000000000000//! 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.3.2/src/vobsub/mpeg2/pes.rs000066400000000000000000000244551474076126700174510ustar00rootroot00000000000000//! # 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 fmt::Debug for Packet<'_> { 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.3.2/src/vobsub/mpeg2/ps.rs000066400000000000000000000126071474076126700173000ustar00rootroot00000000000000//! # 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.3.2/src/vobsub/palette.rs000066400000000000000000000064371474076126700173060ustar00rootroot00000000000000use image::Rgb; use nom::{ bytes::complete::{tag, take_while_m_n}, combinator::map_res, multi::separated_list0, sequence::tuple, IResult, }; use super::VobSubError; pub const DEFAULT_PALETTE: 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]), ]; /// 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 palette 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.3.2/src/vobsub/probe.rs000066400000000000000000000030171474076126700167460ustar00rootroot00000000000000//! 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.3.2/src/vobsub/sub.rs000066400000000000000000000457271474076126700164460ustar00rootroot00000000000000//! # Subtitle data parsing. //! //! For background, see [this documentation on the DVD subtitle format][subs]. //! //! [subs]: http://sam.zoy.org/writings/dvd/subtitles/ use super::{decoder::VobSubDecoder, img::VobSubIndexedImage, mpeg2::ps, VobSubError}; use crate::{ content::{Area, AreaValues}, time::TimeSpan, util::BytesFormatter, vobsub::{ img::{VobSubRleImage, VobSubRleImageData}, IResultExt, }, }; use iter_fixed::IntoIteratorFixed; 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::Debug, marker::PhantomData}; use thiserror::Error; /// 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, }, )) } /// 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<'a, D, T>(raw_data: &'a [u8], base_time: f64) -> Result where T: Debug, D: VobSubDecoder<'a, Output = T>, { // 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 = usize::from(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. let end = initial_control_offset + 2; // reverse palette & alpha once for all let palette = palette.into_iter_fixed().rev().collect(); let alpha = alpha.into_iter_fixed().rev().collect(); let image_data = VobSubRleImageData::new(raw_data, rle_offsets, end)?; let rle_image = VobSubRleImage::new(area, palette, alpha, image_data); // Return our parsed subtitle. let result = D::from_data(start_time, end_time, force, rle_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. pub struct VobsubParser<'a, Decoder> { pes_packets: ps::PesPackets<'a>, phantom_data: PhantomData, } impl<'a, Decoder> VobsubParser<'a, Decoder> { /// To parse a `vobsub` (.sub) file content. /// Return an iterator over the subtitles in this data stream. #[must_use] pub const fn new(input: &'a [u8]) -> Self { Self { pes_packets: ps::pes_packets(input), phantom_data: PhantomData, } } // Read all pes_packets needed to parse a subtitle. fn next_sub_packet(&mut self) -> Option), VobSubError>> { profiling::scope!("VobsubParser next_sub_packet"); // 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); } Some(Ok((base_time, sub_packet))) } } impl Iterator for VobsubParser<'_, D> { type Item = Result<(TimeSpan, VobSubIndexedImage), VobSubError>; fn next(&mut self) -> Option { profiling::scope!("VobsubParser next"); let (base_time, sub_packet) = try_iter!(self.next_sub_packet()); let subtitle = subtitle::<(TimeSpan, VobSubIndexedImage), _>(&sub_packet, base_time); // Parse our subtitle buffer. Some(subtitle) } } #[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::*; use crate::image::ImageArea; //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 = VobsubParser::<(TimeSpan, VobSubIndexedImage)>::new(&buffer); let (time_span, img) = subs.next().expect("missing sub 1").unwrap(); assert!(time_span.start.to_secs() - 49.4 < 0.1); assert!(time_span.end.to_secs() - 50.9 < 0.1); //assert!(!sub1.force); assert_eq!( img.area(), Area::try_from(AreaValues { x1: 750, y1: 916, x2: 1172, y2: 966 }) .unwrap() ); assert_eq!(*img.palette(), [0, 1, 3, 0]); assert_eq!(*img.alpha(), [0, 15, 15, 15]); subs.next().expect("missing sub 2").unwrap(); assert!(subs.next().is_none()); } #[test] fn parse_subtitles_times() { //use env_logger; use std::fs; use std::io::prelude::*; use crate::image::ImageArea; //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 = VobsubParser::::new(&buffer); let (time_span, img) = subs.next().expect("missing sub 1").unwrap(); assert!(time_span.start.to_secs() - 49.4 < 0.1); assert!(time_span.end.to_secs() - 50.9 < 0.1); //assert!(!sub1.force); assert_eq!( img.area(), Area::try_from(AreaValues { x1: 750, y1: 916, x2: 1172, y2: 966 }) .unwrap() ); assert_eq!(*img.palette(), [0, 1, 3, 0]); assert_eq!(*img.alpha(), [0, 15, 15, 15]); 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); } }