├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── example1.rs └── example2.rs ├── rustfmt.toml └── src ├── errors.rs ├── formats ├── common.rs ├── idx.rs ├── microdvd.rs ├── mod.rs ├── srt.rs ├── ssa.rs └── vobsub.rs ├── lib.rs └── timetypes.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | 4 | data 5 | .* 6 | !.gitignore 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "subparse" 3 | version = "0.7.0" 4 | authors = ["kaegi "] 5 | description = "Load, change and write common subtitle formats (srt/ass/idx/sub)" 6 | repository = "https://github.com/kaegi/subparse" 7 | documentation = "https://docs.rs/subparse" 8 | readme = "README.md" 9 | keywords = ["subtitle", "parse", "library", "write"] 10 | license = "MPL-2.0" 11 | edition = "2018" 12 | 13 | 14 | [dependencies] 15 | combine = "2.5.1" 16 | vobsub = "0.2.3" 17 | itertools = "0.8.0" 18 | encoding_rs = "0.8.28" 19 | failure = "0.1.8" 20 | chardet = "0.2.4" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `subparse` is a Rust library that lets use load, change and store subtitle files in various formats. Formatting and other data will be preserved. 4 | 5 | You can find an examples how to use this library under `examples/`. 6 | 7 | Currently supported are: 8 | 9 | - SubStationAlpha `.ssa`/`.ass` 10 | - MicroDVD `.sub` 11 | - SubRip `.srt` 12 | - VobSub `.idx` and `.sub` 13 | 14 | [Documentation](https://docs.rs/subparse) 15 | 16 | [Crates.io](https://crates.io/crates/subparse) 17 | 18 | ## How to use the library 19 | Add this to your `Cargo.toml`: 20 | 21 | ```toml 22 | [dependencies] 23 | subparse = "0.7.0" 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/example1.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | use subparse::timetypes::TimeDelta; 4 | use subparse::SubtitleEntry; 5 | use subparse::{get_subtitle_format, parse_str}; 6 | 7 | /// This function reads the content of a file to a `String`. 8 | fn read_file(path: &Path) -> String { 9 | use std::io::Read; 10 | let mut file = std::fs::File::open(path).unwrap(); 11 | let mut s = String::new(); 12 | file.read_to_string(&mut s).unwrap(); 13 | s 14 | } 15 | 16 | fn main() { 17 | // your setup goes here 18 | let path = PathBuf::from("path/your_example_file.ssa"); 19 | let file_content: String = read_file(&path); // your own load routine 20 | 21 | // parse the file 22 | let format = get_subtitle_format(path.extension(), file_content.as_bytes()).expect("unknown format"); 23 | let mut subtitle_file = parse_str(format, &file_content, 25.0).expect("parser error"); 24 | let mut subtitle_entries: Vec = subtitle_file.get_subtitle_entries().expect("unexpected error"); 25 | 26 | // shift all subtitle entries by 1 minute and append "subparse" to each subtitle line 27 | for subtitle_entry in &mut subtitle_entries { 28 | subtitle_entry.timespan += TimeDelta::from_mins(1); 29 | 30 | // image based subtitles like .idx (VobSub) don't have text, so 31 | // a text is optional 32 | if let Some(ref mut line_ref) = subtitle_entry.line { 33 | line_ref.push_str("subparse"); 34 | } 35 | } 36 | 37 | // update the entries in the subtitle file 38 | subtitle_file.update_subtitle_entries(&subtitle_entries).expect("unexpected error"); 39 | 40 | // print the corrected file to stdout 41 | let data: Vec = subtitle_file.to_data().expect("unexpected errror"); 42 | let data_string = String::from_utf8(data).expect("UTF-8 conversion error"); 43 | println!("{}", data_string); 44 | } 45 | -------------------------------------------------------------------------------- /examples/example2.rs: -------------------------------------------------------------------------------- 1 | extern crate subparse; 2 | 3 | use subparse::timetypes::{TimePoint, TimeSpan}; 4 | use subparse::{SrtFile, SubtitleFileInterface}; 5 | 6 | fn main() { 7 | // example how to create a fresh .srt file 8 | let lines = vec![ 9 | ( 10 | TimeSpan::new(TimePoint::from_msecs(1500), TimePoint::from_msecs(3700)), 11 | "line1".to_string(), 12 | ), 13 | ( 14 | TimeSpan::new(TimePoint::from_msecs(4500), TimePoint::from_msecs(8700)), 15 | "line2".to_string(), 16 | ), 17 | ]; 18 | let file = SrtFile::create(lines).unwrap(); 19 | 20 | // generate file content 21 | let srt_string = String::from_utf8(file.to_data().unwrap()).unwrap(); 22 | println!("{}", srt_string); 23 | } 24 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | write-mode="overwrite" 2 | reorder_imports = true 3 | format_strings = false 4 | chain_overflow_last = false 5 | chain_indent = "Visual" 6 | single_line_if_else = true 7 | same_line_if_else = false 8 | fn_single_line = false 9 | max_width = 150 10 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use crate::SubtitleFormat; 6 | use failure::Backtrace; 7 | use failure::Context; 8 | use failure::Fail; 9 | use std::fmt; 10 | 11 | pub use crate::formats::idx::errors as idx_errors; 12 | pub use crate::formats::microdvd::errors as mdvd_errors; 13 | 14 | pub use crate::formats::srt::errors as srt_errors; 15 | pub use crate::formats::ssa::errors as ssa_errors; 16 | pub use crate::formats::vobsub::errors as vob_errors; 17 | 18 | /// A result type that can be used wide for error handling. 19 | pub type Result = std::result::Result; 20 | 21 | /// The error structure which containes, a backtrace, causes and the error kind enum variant. 22 | #[derive(Debug)] 23 | pub struct Error { 24 | inner: Context, 25 | } 26 | 27 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] 28 | /// Error kind for a crate-wide error. 29 | pub enum ErrorKind { 30 | /// Parsing error 31 | ParsingError, 32 | 33 | /// The file format is not supported by this library. 34 | UnknownFileFormat, 35 | 36 | /// The file format is not supported by this library. 37 | DecodingError, 38 | 39 | /// The file format is not supported by this library. 40 | EncodingDetectionError, 41 | 42 | /// The attempted operation does not work on binary subtitle formats. 43 | TextFormatOnly, 44 | 45 | /// The attempted operation does not work on this format (not supported in this version of this library). 46 | UpdatingEntriesNotSupported { 47 | /// The format for which updating the subtitle entries is not supported. 48 | format: SubtitleFormat, 49 | }, 50 | } 51 | 52 | impl fmt::Display for ErrorKind { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | match self { 55 | ErrorKind::ParsingError => write!(f, "parsing the subtitle data failed"), 56 | ErrorKind::UnknownFileFormat => write!( 57 | f, 58 | "unknown file format, only SubRip (.srt), SubStationAlpha (.ssa/.ass) and VobSub (.idx and .sub) are supported at the moment" 59 | ), 60 | ErrorKind::DecodingError => write!(f, "error while decoding subtitle from bytes to string (wrong charset encoding?)"), 61 | ErrorKind::EncodingDetectionError => write!(f, "could not determine character encoding from byte array (manually supply character encoding?)"), 62 | ErrorKind::TextFormatOnly => write!(f, "operation does not work on binary subtitle formats (only text formats)"), 63 | ErrorKind::UpdatingEntriesNotSupported { format } => write!( 64 | f, 65 | "updating subtitles is not implemented or supported by the `subparse` library for this format: {}", 66 | format.get_name() 67 | ), 68 | } 69 | } 70 | } 71 | 72 | impl Fail for Error { 73 | fn cause(&self) -> Option<&dyn Fail> { 74 | self.inner.cause() 75 | } 76 | 77 | fn backtrace(&self) -> Option<&Backtrace> { 78 | self.inner.backtrace() 79 | } 80 | } 81 | 82 | impl fmt::Display for Error { 83 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 84 | fmt::Display::fmt(&self.inner, f) 85 | } 86 | } 87 | 88 | impl Error { 89 | /// Returns the actual error kind for this error. 90 | pub fn kind(&self) -> ErrorKind { 91 | *self.inner.get_context() 92 | } 93 | } 94 | 95 | impl From for Error { 96 | fn from(kind: ErrorKind) -> Error { 97 | Error { inner: Context::new(kind) } 98 | } 99 | } 100 | 101 | impl From> for Error { 102 | fn from(inner: Context) -> Error { 103 | Error { inner: inner } 104 | } 105 | } 106 | 107 | // see https://docs.rs/error-chain/0.8.1/error_chain/ 108 | /*#[cfg_attr(rustfmt, rustfmt_skip)] 109 | error_chain! { 110 | foreign_links { 111 | FromUtf8Error(::std::string::FromUtf8Error) 112 | /// Converting byte-stream to string failed. 113 | ; 114 | } 115 | 116 | 117 | links { 118 | SsaError(ssa_errors::Error, ssa_errors::ErrorKind) 119 | /// Parsing a `.ssa`/`.ass` file failed. 120 | ; 121 | 122 | IdxError(idx_errors::Error, idx_errors::ErrorKind) 123 | /// Parsing a `.idx` file failed. 124 | ; 125 | 126 | SrtError(srt_errors::Error, srt_errors::ErrorKind) 127 | /// Parsing a `.srt` file failed. 128 | ; 129 | 130 | VobError(vob_errors::Error, vob_errors::ErrorKind) 131 | /// Parsing a `.sub` (VobSub) file failed. 132 | ; 133 | 134 | MdvdError(mdvd_errors::Error, mdvd_errors::ErrorKind) 135 | /// Parsing a `.sub` (MicroDVD) file failed. 136 | ; 137 | } 138 | 139 | errors { 140 | /// The file format is not supported by this library. 141 | UnknownFileFormat { 142 | description("unknown file format, only SubRip (.srt), SubStationAlpha (.ssa/.ass) and VobSub (.idx and .sub) are supported at the moment") 143 | } 144 | 145 | /// The file format is not supported by this library. 146 | DecodingError { 147 | description("error while decoding subtitle from bytes to string (wrong charset encoding?)") 148 | } 149 | 150 | /// The attempted operation does not work on binary subtitle formats. 151 | TextFormatOnly { 152 | description("operation does not work on binary subtitle formats (only text formats)") 153 | } 154 | 155 | /// The attempted operation does not work on this format (not supported in this version of this library). 156 | UpdatingEntriesNotSupported(format: SubtitleFormat) { 157 | description("updating subtitles is not implemented or supported by the `subparse` library for this format") 158 | display("updating subtitles is not implemented or supported by the `subparse` library for this format: {}", format.get_name()) 159 | } 160 | } 161 | }*/ 162 | 163 | #[macro_use] 164 | /// Creates the `Error`-context type for an ErrorKind and associated conversion methods. 165 | macro_rules! define_error { 166 | ($error:ident, $kind:ident) => { 167 | use failure::Fail; 168 | use failure::{Backtrace, Context}; 169 | use std::fmt; 170 | 171 | /// The error structure which containes, a backtrace, causes and the error kind enum variant. 172 | #[derive(Debug)] 173 | pub struct $error { 174 | inner: Context<$kind>, 175 | } 176 | 177 | impl Fail for $error { 178 | fn cause(&self) -> Option<&dyn Fail> { 179 | self.inner.cause() 180 | } 181 | 182 | fn backtrace(&self) -> Option<&Backtrace> { 183 | self.inner.backtrace() 184 | } 185 | } 186 | 187 | impl fmt::Display for $error { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | fmt::Display::fmt(&self.inner, f) 190 | } 191 | } 192 | 193 | impl $error { 194 | /// Get inner error enum variant. 195 | pub fn kind(&self) -> &$kind { 196 | self.inner.get_context() 197 | } 198 | } 199 | 200 | impl From<$kind> for $error { 201 | fn from(kind: $kind) -> $error { 202 | $error { inner: Context::new(kind) } 203 | } 204 | } 205 | 206 | impl From> for $error { 207 | fn from(inner: Context<$kind>) -> $error { 208 | $error { inner: inner } 209 | } 210 | } 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /src/formats/common.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use combine::char::*; 6 | use combine::combinator::*; 7 | use combine::primitives::{ParseError, ParseResult, Parser, Stream}; 8 | use std::fmt::Display; 9 | use std::str::FromStr; 10 | 11 | type CustomCharParser = Expected bool>>; 12 | 13 | /// Returns the string without BOMs. Unchanged if string does not start with one. 14 | pub fn split_bom(s: &str) -> (&str, &str) { 15 | if s.as_bytes().iter().take(3).eq([0xEF, 0xBB, 0xBF].iter()) { 16 | s.split_at(3) 17 | } else if s.as_bytes().iter().take(2).eq([0xFE, 0xFF].iter()) { 18 | s.split_at(2) 19 | } else { 20 | ("", s) 21 | } 22 | } 23 | 24 | #[test] 25 | #[allow(unsafe_code)] 26 | fn test_split_bom() { 27 | let bom1_vec = &[0xEF, 0xBB, 0xBF]; 28 | let bom2_vec = &[0xFE, 0xFF]; 29 | let bom1 = unsafe { ::std::str::from_utf8_unchecked(bom1_vec) }; 30 | let bom2 = unsafe { ::std::str::from_utf8_unchecked(bom2_vec) }; 31 | 32 | // Rust doesn't seem to let us create a BOM as str in a safe way. 33 | assert_eq!( 34 | split_bom(unsafe { ::std::str::from_utf8_unchecked(&[0xEF, 0xBB, 0xBF, 'a' as u8, 'b' as u8, 'c' as u8]) }), 35 | (bom1, "abc") 36 | ); 37 | assert_eq!( 38 | split_bom(unsafe { ::std::str::from_utf8_unchecked(&[0xFE, 0xFF, 'd' as u8, 'e' as u8, 'g' as u8]) }), 39 | (bom2, "deg") 40 | ); 41 | assert_eq!(split_bom("bla"), ("", "bla")); 42 | assert_eq!(split_bom(""), ("", "")); 43 | } 44 | 45 | /// Parses whitespaces and tabs. 46 | #[inline] 47 | #[allow(trivial_casts)] 48 | pub fn ws() -> CustomCharParser 49 | where 50 | I: Stream, 51 | { 52 | fn f(c: char) -> bool { 53 | c == ' ' || c == '\t' 54 | } 55 | satisfy(f as fn(_) -> _).expected("tab or space") 56 | } 57 | 58 | /// Matches a positive or negative intger number. 59 | pub fn number_i64(input: I) -> ParseResult 60 | where 61 | I: Stream, 62 | { 63 | (optional(char('-')), many1(digit())) 64 | .map(|(a, c): (Option<_>, String)| { 65 | // we provide a string that only contains digits: this unwrap should never fail 66 | let i: i64 = FromStr::from_str(&c).unwrap(); 67 | match a { 68 | Some(_) => -i, 69 | None => i, 70 | } 71 | }) 72 | .expected("positive or negative number") 73 | .parse_stream(input) 74 | } 75 | 76 | /// Create a single-line-error string from a combine parser error. 77 | pub fn parse_error_to_string(p: ParseError) -> String 78 | where 79 | I: Stream, 80 | R: PartialEq + Clone + Display, 81 | P: Ord + Display, 82 | { 83 | p.to_string() 84 | .trim() 85 | .lines() 86 | .fold("".to_string(), |a, b| if a.is_empty() { b.to_string() } else { a + "; " + b }) 87 | } 88 | 89 | /// This function does a very common task for non-destructive parsers: merging mergable consecutive file parts. 90 | /// 91 | /// Each file has some "filler"-parts in it (unimportant information) which only get stored to reconstruct the 92 | /// original file. Two consecutive filler parts (their strings) can be merged. This function abstracts over the 93 | /// specific file part type. 94 | pub fn dedup_string_parts(v: Vec, mut extract_fn: F) -> Vec 95 | where 96 | F: FnMut(&mut T) -> Option<&mut String>, 97 | { 98 | let mut result = Vec::new(); 99 | for mut part in v { 100 | let mut push_part = true; 101 | if let Some(last_part) = result.last_mut() { 102 | if let Some(exchangeable_text) = extract_fn(last_part) { 103 | if let Some(new_text) = extract_fn(&mut part) { 104 | exchangeable_text.push_str(new_text); 105 | push_part = false; 106 | } 107 | } 108 | } 109 | 110 | if push_part { 111 | result.push(part); 112 | } 113 | } 114 | 115 | result 116 | } 117 | 118 | // used in `get_lines_non_destructive()` 119 | type SplittedLine = (String /* string */, String /* newline string like \n or \r\n */); 120 | 121 | /// Iterates over all lines in `s` and calls the `process_line` closure for every line and line ending. 122 | /// This ensures that we can reconstruct the file with correct line endings. 123 | /// 124 | /// This will also accept the line ending `\r` (not within `\r\n`) to avoid error handling. 125 | pub fn get_lines_non_destructive(s: &str) -> Vec { 126 | let mut result = Vec::new(); 127 | let mut rest = s; 128 | loop { 129 | if rest.is_empty() { 130 | return result; 131 | } 132 | 133 | match rest.char_indices().find(|&(_, c)| c == '\r' || c == '\n') { 134 | Some((idx, _)) => { 135 | let (line_str, new_rest) = rest.split_at(idx); 136 | rest = new_rest; 137 | 138 | let line = line_str.to_string(); 139 | if rest.starts_with("\r\n") { 140 | result.push((line, "\r\n".to_string())); 141 | rest = &rest[2..]; 142 | } else if rest.starts_with('\n') { 143 | result.push((line, "\n".to_string())); 144 | rest = &rest[1..]; 145 | } else if rest.starts_with('\r') { 146 | // we only treat this as valid line ending to avoid error handling 147 | result.push((line, "\r".to_string())); 148 | rest = &rest[1..]; 149 | } 150 | } 151 | None => { 152 | result.push((rest.to_string(), "".to_string())); 153 | return result; 154 | } 155 | } 156 | } 157 | } 158 | 159 | #[test] 160 | fn get_lines_non_destructive_test0() { 161 | let lines = ["", "aaabb", "aaabb\r\nbcccc\n\r\n ", "aaabb\r\nbcccc"]; 162 | for &full_line in lines.iter() { 163 | let joined: String = get_lines_non_destructive(full_line) 164 | .into_iter() 165 | .flat_map(|(s1, s2)| vec![s1, s2].into_iter()) 166 | .collect(); 167 | assert_eq!(full_line, joined); 168 | } 169 | } 170 | 171 | /// Trim a string left and right, but also preserve the white-space characters. The 172 | /// seconds element in the returned tuple contains the non-whitespace string. 173 | pub fn trim_non_destructive(s: &str) -> (String, String, String) { 174 | let (begin, rest) = trim_left(s); 175 | let (end, rest2) = trim_left(&rest.chars().rev().collect::()); 176 | (begin, rest2.chars().rev().collect(), end.chars().rev().collect()) 177 | } 178 | 179 | /// Splits a string in whitespace string and the rest " hello " -> (" ", "hello "). 180 | fn trim_left(s: &str) -> (String, String) { 181 | (many(ws()), many(r#try(any())), eof()) 182 | .map(|t| (t.0, t.1)) 183 | .parse(s) 184 | .expect("the trim parser should accept any input") 185 | .0 186 | } 187 | -------------------------------------------------------------------------------- /src/formats/idx.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use self::errors::ErrorKind::*; // the crate wide error type (we use a custom error type here) 6 | use self::errors::*; 7 | use super::common::*; 8 | use crate::{SubtitleEntry, SubtitleFileInterface}; 9 | 10 | use crate::errors::Result as SubtitleParserResult; 11 | use combine::char::*; 12 | use combine::combinator::*; 13 | use combine::primitives::Parser; 14 | 15 | use failure::ResultExt; 16 | 17 | use crate::timetypes::{TimeDelta, TimePoint, TimeSpan}; 18 | use std::iter::once; 19 | 20 | /// `.idx`-parser-specific errors 21 | #[allow(missing_docs)] 22 | pub mod errors { 23 | pub type Result = std::result::Result; 24 | 25 | define_error!(Error, ErrorKind); 26 | 27 | #[derive(PartialEq, Debug, Fail)] 28 | pub enum ErrorKind { 29 | #[fail(display = "parsing the line `{}` failed because of `{}`", line_num, msg)] 30 | IdxLineParseError { line_num: usize, msg: String }, 31 | } 32 | } 33 | 34 | // //////////////////////////////////////////////////////////////////////////////////////////////// 35 | // .idx file parts 36 | 37 | #[derive(Debug, Clone)] 38 | enum IdxFilePart { 39 | /// Spaces, field information, comments, unimportant fields, ... 40 | Filler(String), 41 | 42 | /// Represents a parsed time string like "00:42:20:204". 43 | Timestamp(TimePoint), 44 | } 45 | 46 | // //////////////////////////////////////////////////////////////////////////////////////////////// 47 | // .idx file 48 | 49 | /// Represents a reconstructable `.idx` file. 50 | /// 51 | /// All (for this project) unimportant information are saved into `IdxFilePart::Filler(...)`, so 52 | /// a timespan-altered file still has the same meta-information. 53 | #[derive(Debug, Clone)] 54 | pub struct IdxFile { 55 | v: Vec, 56 | } 57 | 58 | impl IdxFile { 59 | fn new(v: Vec) -> IdxFile { 60 | // cleans up multiple fillers after another 61 | let new_file_parts = dedup_string_parts(v, |part: &mut IdxFilePart| match *part { 62 | IdxFilePart::Filler(ref mut text) => Some(text), 63 | _ => None, 64 | }); 65 | IdxFile { v: new_file_parts } 66 | } 67 | } 68 | 69 | impl SubtitleFileInterface for IdxFile { 70 | fn get_subtitle_entries(&self) -> SubtitleParserResult> { 71 | let timings: Vec<_> = self 72 | .v 73 | .iter() 74 | .filter_map(|file_part| match *file_part { 75 | IdxFilePart::Filler(_) => None, 76 | IdxFilePart::Timestamp(t) => Some(t), 77 | }) 78 | .collect(); 79 | 80 | Ok(match timings.last() { 81 | Some(&last_timing) => { 82 | // .idx files do not store timespans. Every subtitle is shown until the next subtitle 83 | // starts. Mpv shows the last subtitle for exactly one minute. 84 | let next_timings = timings.iter().cloned().skip(1).chain(once(last_timing + TimeDelta::from_mins(1))); 85 | timings 86 | .iter() 87 | .cloned() 88 | .zip(next_timings) 89 | .map(|time_tuple| TimeSpan::new(time_tuple.0, time_tuple.1)) 90 | .map(SubtitleEntry::from) 91 | .collect() 92 | } 93 | None => { 94 | // no timings 95 | Vec::new() 96 | } 97 | }) 98 | } 99 | 100 | fn update_subtitle_entries(&mut self, ts: &[SubtitleEntry]) -> SubtitleParserResult<()> { 101 | let mut count = 0; 102 | for file_part_ref in &mut self.v { 103 | match *file_part_ref { 104 | IdxFilePart::Filler(_) => {} 105 | IdxFilePart::Timestamp(ref mut this_ts_ref) => { 106 | *this_ts_ref = ts[count - 1].timespan.start; 107 | count += 1; 108 | } 109 | } 110 | } 111 | 112 | assert_eq!(count, ts.len()); // required by specification of this function 113 | Ok(()) 114 | } 115 | 116 | fn to_data(&self) -> SubtitleParserResult> { 117 | // timing to string like "00:03:28:308" 118 | let fn_timing_to_string = |t: TimePoint| { 119 | let p = if t.msecs() < 0 { -t } else { t }; 120 | format!( 121 | "{}{:02}:{:02}:{:02}:{:03}", 122 | if t.msecs() < 0 { "-" } else { "" }, 123 | p.hours(), 124 | p.mins_comp(), 125 | p.secs_comp(), 126 | p.msecs_comp() 127 | ) 128 | }; 129 | 130 | let fn_file_part_to_string = |part: &IdxFilePart| { 131 | use self::IdxFilePart::*; 132 | match *part { 133 | Filler(ref t) => t.clone(), 134 | Timestamp(t) => fn_timing_to_string(t), 135 | } 136 | }; 137 | 138 | let result: String = self.v.iter().map(fn_file_part_to_string).collect(); 139 | 140 | Ok(result.into_bytes()) 141 | } 142 | } 143 | 144 | // //////////////////////////////////////////////////////////////////////////////////////////////// 145 | // .idx parser 146 | 147 | impl IdxFile { 148 | /// Parse a `.idx` subtitle string to `IdxFile`. 149 | pub fn parse(s: &str) -> SubtitleParserResult { 150 | Ok(Self::parse_inner(s).with_context(|_| crate::ErrorKind::ParsingError)?) 151 | } 152 | } 153 | 154 | // implement parsing functions 155 | impl IdxFile { 156 | fn parse_inner(i: &str) -> Result { 157 | // remove utf-8 BOM 158 | let mut result = Vec::new(); 159 | let (bom, s) = split_bom(i); 160 | result.push(IdxFilePart::Filler(bom.to_string())); 161 | 162 | for (line_num, (line, newl)) in get_lines_non_destructive(s).into_iter().enumerate() { 163 | let mut file_parts = Self::parse_line(line_num, line)?; 164 | result.append(&mut file_parts); 165 | result.push(IdxFilePart::Filler(newl)); 166 | } 167 | 168 | Ok(IdxFile::new(result)) 169 | } 170 | 171 | fn parse_line(line_num: usize, s: String) -> Result> { 172 | if !s.trim_start().starts_with("timestamp:") { 173 | return Ok(vec![IdxFilePart::Filler(s)]); 174 | } 175 | 176 | ( 177 | many(ws()), 178 | string("timestamp:"), 179 | many(ws()), 180 | many(or(digit(), token(':'))), 181 | many(r#try(any())), 182 | eof(), 183 | ) 184 | .map( 185 | |(ws1, s1, ws2, timestamp_str, s2, _): (String, &str, String, String, String, ())| -> Result> { 186 | let mut result = Vec::::new(); 187 | result.push(IdxFilePart::Filler(ws1)); 188 | result.push(IdxFilePart::Filler(s1.to_string())); 189 | result.push(IdxFilePart::Filler(ws2)); 190 | result.push(IdxFilePart::Timestamp(Self::parse_timestamp(line_num, timestamp_str.as_str())?)); 191 | result.push(IdxFilePart::Filler(s2.to_string())); 192 | Ok(result) 193 | }, 194 | ) 195 | .parse(s.as_str()) 196 | .map_err(|e| IdxLineParseError { 197 | line_num, 198 | msg: parse_error_to_string(e), 199 | })? 200 | .0 201 | } 202 | 203 | /// Parse an .idx timestamp like `00:41:36:961`. 204 | fn parse_timestamp(line_num: usize, s: &str) -> Result { 205 | ( 206 | parser(number_i64), 207 | token(':'), 208 | parser(number_i64), 209 | token(':'), 210 | parser(number_i64), 211 | token(':'), 212 | parser(number_i64), 213 | eof(), 214 | ) 215 | .map(|(hours, _, mins, _, secs, _, msecs, _)| TimePoint::from_components(hours, mins, secs, msecs)) 216 | .parse(s) // <- return type is ParseResult<(Timing, &str)> 217 | .map(|(file_part, _)| file_part) 218 | .map_err(|e| { 219 | IdxLineParseError { 220 | line_num, 221 | msg: parse_error_to_string(e), 222 | } 223 | .into() 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/formats/microdvd.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use self::errors::ErrorKind::*; 6 | use self::errors::*; 7 | use crate::{SubtitleEntry, SubtitleFileInterface}; 8 | 9 | use crate::errors::Result as SubtitleParserResult; 10 | use crate::formats::common::*; 11 | use combine::char::char; 12 | use combine::combinator::{eof, many, parser as p, satisfy, sep_by}; 13 | use combine::primitives::Parser; 14 | 15 | use itertools::Itertools; 16 | use std::borrow::Cow; 17 | use std::collections::HashSet; 18 | 19 | use failure::ResultExt; 20 | 21 | use crate::timetypes::{TimePoint, TimeSpan}; 22 | use std::collections::LinkedList; 23 | 24 | /// Errors specific to `.sub`(`MicroDVD`)-parsing. 25 | #[allow(missing_docs)] 26 | pub mod errors { 27 | pub type Result = std::result::Result; 28 | 29 | define_error!(Error, ErrorKind); 30 | 31 | #[derive(PartialEq, Debug, Fail)] 32 | pub enum ErrorKind { 33 | #[fail(display = "expected subtittle line, found `{}`", line)] 34 | ExpectedSubtitleLine { line: String }, 35 | #[fail(display = "parse error at line `{}`", line_num)] 36 | ErrorAtLine { line_num: usize }, 37 | } 38 | } 39 | 40 | /// Represents a formatting like "{y:i}" (display text in italics). 41 | /// 42 | /// TODO: `MdvdFormatting` is a stub for the future where this enum holds specialized variants for different options. 43 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 44 | enum MdvdFormatting { 45 | /// A format option that is not directly supported. 46 | Unknown(String), 47 | } 48 | 49 | impl From for MdvdFormatting { 50 | fn from(f: String) -> MdvdFormatting { 51 | MdvdFormatting::Unknown(Self::lowercase_first_char(&f)) 52 | } 53 | } 54 | 55 | impl MdvdFormatting { 56 | /// Is this a single line formatting (e.g. `y:i`) or a multi-line formatting (e.g `Y:i`)? 57 | fn is_container_line_formatting(f: &str) -> bool { 58 | f.chars().next().and_then(|c| Some(c.is_uppercase())).unwrap_or(false) 59 | } 60 | 61 | /// Applies `to_lowercase()` to first char, leaves the rest of the characters untouched. 62 | fn lowercase_first_char(s: &str) -> String { 63 | let mut c = s.chars(); 64 | match c.next() { 65 | None => String::new(), 66 | Some(f) => f.to_lowercase().collect::() + c.as_str(), 67 | } 68 | } 69 | 70 | /// Applies `to_uppercase()` to first char, leaves the rest of the characters untouched. 71 | fn uppercase_first_char(s: &str) -> String { 72 | let mut c = s.chars(); 73 | match c.next() { 74 | None => String::new(), 75 | Some(f) => f.to_uppercase().collect::() + c.as_str(), 76 | } 77 | } 78 | 79 | fn to_formatting_string_intern(&self) -> String { 80 | match *self { 81 | MdvdFormatting::Unknown(ref s) => s.clone(), 82 | } 83 | } 84 | 85 | /// Convert a `MdvdFormatting` to a string which can be used in `.sub` files. 86 | fn to_formatting_string(&self, multiline: bool) -> String { 87 | let s = self.to_formatting_string_intern(); 88 | if multiline { 89 | Self::uppercase_first_char(&s) 90 | } else { 91 | Self::lowercase_first_char(&s) 92 | } 93 | } 94 | } 95 | 96 | #[derive(Debug, Clone)] 97 | /// Represents a reconstructable `.sub`(`MicroDVD`) file. 98 | pub struct MdvdFile { 99 | /// Number of frames per second of the accociated video (default 25) 100 | /// -> start/end frames can be coverted to timestamps 101 | fps: f64, 102 | 103 | /// all lines and multilines 104 | v: Vec, 105 | } 106 | 107 | /// Holds the description of a line like. 108 | #[derive(Debug, Clone)] 109 | struct MdvdLine { 110 | /// The start frame. 111 | start_frame: i64, 112 | 113 | /// The end frame. 114 | end_frame: i64, 115 | 116 | /// Formatting that affects all contained single lines. 117 | formatting: Vec, 118 | 119 | /// The (dialog) text of the line. 120 | text: String, 121 | } 122 | 123 | impl MdvdLine { 124 | fn to_subtitle_entry(&self, fps: f64) -> SubtitleEntry { 125 | SubtitleEntry { 126 | timespan: TimeSpan::new( 127 | TimePoint::from_msecs((self.start_frame as f64 * 1000.0 / fps) as i64), 128 | TimePoint::from_msecs((self.end_frame as f64 * 1000.0 / fps) as i64), 129 | ), 130 | line: Some(self.text.clone()), 131 | } 132 | } 133 | } 134 | 135 | impl MdvdFile { 136 | /// Parse a `MicroDVD` `.sub` subtitle string to `MdvdFile`. 137 | pub fn parse(s: &str, fps: f64) -> SubtitleParserResult { 138 | Ok(Self::parse_file(s, fps).with_context(|_| crate::ErrorKind::ParsingError)?) 139 | } 140 | } 141 | 142 | /// Implements parse functions. 143 | impl MdvdFile { 144 | fn parse_file(i: &str, fps: f64) -> Result { 145 | let mut result: Vec = Vec::new(); 146 | 147 | // remove utf-8 bom 148 | let (_, s) = split_bom(i); 149 | 150 | for (line_num, line) in s.lines().enumerate() { 151 | // a line looks like "{0}{25}{c:$0000ff}{y:b,u}{f:DeJaVuSans}{s:12}Hello!|{y:i}Hello2!" where 152 | // 0 and 25 are the start and end frames and the other information is the formatting. 153 | let mut lines: Vec = Self::parse_line(line_num, line)?; 154 | result.append(&mut lines); 155 | } 156 | 157 | Ok(MdvdFile { fps: fps, v: result }) 158 | } 159 | 160 | // Parses something like "{0}{25}{C:$0000ff}{y:b,u}{f:DeJaVuSans}{s:12}Hello!|{s:15}Hello2!" 161 | fn parse_line(line_num: usize, line: &str) -> Result> { 162 | // Matches the regex "\{[^}]*\}"; parses something like "{some_info}". 163 | let sub_info = (char('{'), many(satisfy(|c| c != '}')), char('}')) 164 | .map(|(_, info, _): (_, String, _)| info) 165 | .expected("MicroDVD info"); 166 | 167 | // Parse a single line (until separator '|'), something like "{C:$0000ff}{y:b,u}{f:DeJaVuSans}{s:12}Hello!" 168 | // Returns the a tuple of the multiline-formatting, the single-line formatting and the text of the single line. 169 | let single_line = (many(sub_info), many(satisfy(|c| c != '|'))); 170 | 171 | // the '|' char splits single lines 172 | ( 173 | char('{'), 174 | p(number_i64), 175 | char('}'), 176 | char('{'), 177 | p(number_i64), 178 | char('}'), 179 | sep_by(single_line, char('|')), 180 | eof(), 181 | ) 182 | .map(|(_, start_frame, _, _, end_frame, _, fmt_strs_and_lines, ())| (start_frame, end_frame, fmt_strs_and_lines)) 183 | .map(|(start_frame, end_frame, fmt_strs_and_lines): (i64, i64, Vec<(Vec, String)>)| { 184 | Self::construct_mdvd_lines(start_frame, end_frame, fmt_strs_and_lines) 185 | }) 186 | .parse(line) 187 | .map(|x| x.0) 188 | .map_err(|_| Error::from(ExpectedSubtitleLine { line: line.to_string() })) 189 | .with_context(|_| ErrorAtLine { line_num }) 190 | .map_err(Error::from) 191 | } 192 | 193 | /// Construct (possibly multiple) `MdvdLines` from a deconstructed file line 194 | /// like "{C:$0000ff}{y:b,u}{f:DeJaVuSans}{s:12}Hello!|{s:15}Hello2!". 195 | /// 196 | /// The third parameter is for the example 197 | /// like `[(["C:$0000ff", "y:b,u", "f:DeJaVuSans", "s:12"], "Hello!"), (["s:15"], "Hello2!")]. 198 | fn construct_mdvd_lines(start_frame: i64, end_frame: i64, fmt_strs_and_lines: Vec<(Vec, String)>) -> Vec { 199 | // saves all multiline formatting 200 | let mut cline_fmts: Vec = Vec::new(); 201 | 202 | // convert the formatting strings to `MdvdFormatting` objects and split between multi-line and single-line formatting 203 | let fmts_and_lines = fmt_strs_and_lines 204 | .into_iter() 205 | .map(|(fmts, text)| (Self::string_to_formatting(&mut cline_fmts, fmts), text)) 206 | .collect::>(); 207 | 208 | // now we also have all multi-line formattings in `cline_fmts` 209 | 210 | // finish creation of `MdvdLine`s 211 | fmts_and_lines 212 | .into_iter() 213 | .map(|(sline_fmts, text)| MdvdLine { 214 | start_frame: start_frame, 215 | end_frame: end_frame, 216 | text: text, 217 | formatting: cline_fmts.clone().into_iter().chain(sline_fmts.into_iter()).collect(), 218 | }) 219 | .collect() 220 | } 221 | 222 | /// Convert `MicroDVD` formatting strings to `MdvdFormatting` objects. 223 | /// 224 | /// Move multiline formattings and single line formattings into different vectors. 225 | fn string_to_formatting(multiline_formatting: &mut Vec, fmts: Vec) -> Vec { 226 | // split multiline-formatting (e.g "Y:b") and single-line formatting (e.g "y:b") 227 | let (cline_fmts_str, sline_fmts_str): (Vec<_>, Vec<_>) = fmts 228 | .into_iter() 229 | .partition(|fmt_str| MdvdFormatting::is_container_line_formatting(fmt_str)); 230 | 231 | multiline_formatting.extend(&mut cline_fmts_str.into_iter().map(MdvdFormatting::from)); 232 | sline_fmts_str.into_iter().map(MdvdFormatting::from).collect() 233 | } 234 | } 235 | 236 | impl SubtitleFileInterface for MdvdFile { 237 | fn get_subtitle_entries(&self) -> SubtitleParserResult> { 238 | Ok(self.v.iter().map(|line| line.to_subtitle_entry(self.fps)).collect()) 239 | } 240 | 241 | fn update_subtitle_entries(&mut self, new_subtitle_entries: &[SubtitleEntry]) -> SubtitleParserResult<()> { 242 | assert_eq!(new_subtitle_entries.len(), self.v.len()); 243 | 244 | let mut iter = new_subtitle_entries.iter().peekable(); 245 | for line in &mut self.v { 246 | let peeked = iter.next().unwrap(); 247 | 248 | line.start_frame = (peeked.timespan.start.secs_f64() * self.fps) as i64; 249 | line.end_frame = (peeked.timespan.end.secs_f64() * self.fps) as i64; 250 | 251 | if let Some(ref text) = peeked.line { 252 | line.text = text.clone(); 253 | } 254 | } 255 | 256 | Ok(()) 257 | } 258 | 259 | fn to_data(&self) -> SubtitleParserResult> { 260 | let mut sorted_list = self.v.clone(); 261 | sorted_list.sort_by_key(|line| (line.start_frame, line.end_frame)); 262 | 263 | let mut result: LinkedList> = LinkedList::new(); 264 | 265 | for (gi, group_iter) in sorted_list 266 | .into_iter() 267 | .group_by(|line| (line.start_frame, line.end_frame)) 268 | .into_iter() 269 | .enumerate() 270 | { 271 | if gi != 0 { 272 | result.push_back("\n".into()); 273 | } 274 | 275 | let group: Vec = group_iter.1.collect(); 276 | let group_len = group.len(); 277 | 278 | let (start_frame, end_frame) = group_iter.0; 279 | let (formattings, texts): (Vec>, Vec) = 280 | group.into_iter().map(|line| (line.formatting.into_iter().collect(), line.text)).unzip(); 281 | 282 | // all single lines in the container line "cline" have the same start and end time 283 | // -> the .sub file format let's them be on the same line with "{0}{1000}Text1|Text2" 284 | 285 | // find common formatting in all lines 286 | let common_formatting = if group_len == 1 { 287 | // if this "group" only has a single line, let's say that every formatting is individual 288 | HashSet::new() 289 | } else { 290 | formattings 291 | .iter() 292 | .fold(None, |acc, set| match acc { 293 | None => Some(set.clone()), 294 | Some(acc_set) => Some(acc_set.intersection(set).cloned().collect()), 295 | }) 296 | .unwrap() 297 | }; 298 | 299 | let individual_formattings = formattings 300 | .into_iter() 301 | .map(|formatting| formatting.difference(&common_formatting).cloned().collect()) 302 | .collect::>>(); 303 | 304 | result.push_back("{".into()); 305 | result.push_back(start_frame.to_string().into()); 306 | result.push_back("}".into()); 307 | 308 | result.push_back("{".into()); 309 | result.push_back(end_frame.to_string().into()); 310 | result.push_back("}".into()); 311 | 312 | for formatting in &common_formatting { 313 | result.push_back("{".into()); 314 | result.push_back(formatting.to_formatting_string(true).into()); 315 | result.push_back("}".into()); 316 | } 317 | 318 | for (i, (individual_formatting, text)) in individual_formattings.into_iter().zip(texts.into_iter()).enumerate() { 319 | if i != 0 { 320 | result.push_back("|".into()); 321 | } 322 | 323 | for formatting in individual_formatting { 324 | result.push_back("{".into()); 325 | result.push_back(formatting.to_formatting_string(false).into()); 326 | result.push_back("}".into()); 327 | } 328 | 329 | result.push_back(text.into()); 330 | } 331 | 332 | // ends "group-by-frametime"-loop 333 | } 334 | 335 | Ok(result.into_iter().map(|cow| cow.to_string()).collect::().into_bytes()) 336 | } 337 | } 338 | 339 | #[cfg(test)] 340 | mod tests { 341 | use super::*; 342 | use SubtitleFileInterface; 343 | 344 | /// Parse string with `MdvdFile`, and reencode it with `MdvdFile`. 345 | fn mdvd_reconstruct(s: &str) -> String { 346 | let file = MdvdFile::parse(s, 25.0).unwrap(); 347 | let data = file.to_data().unwrap(); 348 | String::from_utf8(data).unwrap() 349 | } 350 | 351 | /// Parse and re-construct `MicroDVD` files and test them against expected output. 352 | fn test_mdvd(input: &str, expected: &str) { 353 | // if we put the `input` into the parser, we expect a specific (cleaned-up) output 354 | assert_eq!(mdvd_reconstruct(input), expected); 355 | 356 | // if we reconstuct he cleaned-up output, we expect that nothing changes 357 | assert_eq!(mdvd_reconstruct(expected), expected); 358 | } 359 | 360 | #[test] 361 | fn mdvd_test_reconstruction() { 362 | // simple examples 363 | test_mdvd("{0}{25}Hello!", "{0}{25}Hello!"); 364 | test_mdvd("{0}{25}{y:i}Hello!", "{0}{25}{y:i}Hello!"); 365 | test_mdvd("{0}{25}{Y:i}Hello!", "{0}{25}{y:i}Hello!"); 366 | test_mdvd("{0}{25}{Y:i}\n", "{0}{25}{y:i}"); 367 | 368 | // cleanup formattings in a file 369 | test_mdvd("{0}{25}{y:i}Text1|{y:i}Text2", "{0}{25}{Y:i}Text1|Text2"); 370 | test_mdvd("{0}{25}{y:i}Text1\n{0}{25}{y:i}Text2", "{0}{25}{Y:i}Text1|Text2"); 371 | test_mdvd("{0}{25}{y:i}{y:b}Text1\n{0}{25}{y:i}Text2", "{0}{25}{Y:i}{y:b}Text1|Text2"); 372 | test_mdvd("{0}{25}{y:i}{y:b}Text1\n{0}{25}{y:i}Text2", "{0}{25}{Y:i}{y:b}Text1|Text2"); 373 | 374 | // these can't be condensed, because the lines have different times 375 | test_mdvd("{0}{25}{y:i}Text1\n{0}{26}{y:i}Text2", "{0}{25}{y:i}Text1\n{0}{26}{y:i}Text2"); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/formats/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | pub mod common; 6 | pub mod idx; 7 | pub mod microdvd; 8 | pub mod srt; 9 | pub mod ssa; 10 | pub mod vobsub; 11 | 12 | use crate::errors::*; 13 | use crate::SubtitleEntry; 14 | use crate::SubtitleFileInterface; 15 | use encoding_rs::Encoding; 16 | use std::ffi::OsStr; 17 | use chardet::{charset2encoding, detect}; 18 | 19 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 20 | /// All formats which are supported by this library. 21 | pub enum SubtitleFormat { 22 | /// .srt file 23 | SubRip, 24 | 25 | /// .ssa/.ass file 26 | SubStationAlpha, 27 | 28 | /// .idx file 29 | VobSubIdx, 30 | 31 | /// .sub file (`VobSub`/binary) 32 | VobSubSub, 33 | 34 | /// .sub file (`MicroDVD`/text) 35 | MicroDVD, 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | /// Unified wrapper around the all individual subtitle file types. 40 | pub enum SubtitleFile { 41 | /// .srt file 42 | SubRipFile(srt::SrtFile), 43 | 44 | /// .ssa/.ass file 45 | SubStationAlpha(ssa::SsaFile), 46 | 47 | /// .idx file 48 | VobSubIdxFile(idx::IdxFile), 49 | 50 | /// .sub file (`VobSub`/binary) 51 | VobSubSubFile(vobsub::VobFile), 52 | 53 | /// .sub file (`MicroDVD`/text) 54 | MicroDVDFile(microdvd::MdvdFile), 55 | } 56 | 57 | impl SubtitleFile { 58 | /// The subtitle entries can be changed by calling `update_subtitle_entries()`. 59 | pub fn get_subtitle_entries(&self) -> Result> { 60 | match self { 61 | SubtitleFile::SubRipFile(f) => f.get_subtitle_entries(), 62 | SubtitleFile::SubStationAlpha(f) => f.get_subtitle_entries(), 63 | SubtitleFile::VobSubIdxFile(f) => f.get_subtitle_entries(), 64 | SubtitleFile::VobSubSubFile(f) => f.get_subtitle_entries(), 65 | SubtitleFile::MicroDVDFile(f) => f.get_subtitle_entries(), 66 | } 67 | } 68 | 69 | /// Set the entries from the subtitle entries from the `get_subtitle_entries()`. 70 | /// 71 | /// The length of the given input slice should always match the length of the vector length from 72 | /// `get_subtitle_entries()`. This function can not delete/create new entries, but preserves 73 | /// everything else in the file (formatting, authors, ...). 74 | /// 75 | /// If the input entry has `entry.line == None`, the line will not be overwritten. 76 | /// 77 | /// Be aware that .idx files cannot save time_spans_ (a subtitle will be shown between two 78 | /// consecutive timepoints/there are no separate starts and ends) - so the timepoint will be set 79 | /// to the start of the corresponding input-timespan. 80 | pub fn update_subtitle_entries(&mut self, i: &[SubtitleEntry]) -> Result<()> { 81 | match self { 82 | SubtitleFile::SubRipFile(f) => f.update_subtitle_entries(i), 83 | SubtitleFile::SubStationAlpha(f) => f.update_subtitle_entries(i), 84 | SubtitleFile::VobSubIdxFile(f) => f.update_subtitle_entries(i), 85 | SubtitleFile::VobSubSubFile(f) => f.update_subtitle_entries(i), 86 | SubtitleFile::MicroDVDFile(f) => f.update_subtitle_entries(i), 87 | } 88 | } 89 | 90 | /// Returns a byte-stream in the respective format (.ssa, .srt, etc.) with the 91 | /// (probably) altered information. 92 | pub fn to_data(&self) -> Result> { 93 | match self { 94 | SubtitleFile::SubRipFile(f) => f.to_data(), 95 | SubtitleFile::SubStationAlpha(f) => f.to_data(), 96 | SubtitleFile::VobSubIdxFile(f) => f.to_data(), 97 | SubtitleFile::VobSubSubFile(f) => f.to_data(), 98 | SubtitleFile::MicroDVDFile(f) => f.to_data(), 99 | } 100 | } 101 | } 102 | 103 | impl From for SubtitleFile { 104 | fn from(f: srt::SrtFile) -> SubtitleFile { 105 | SubtitleFile::SubRipFile(f) 106 | } 107 | } 108 | 109 | impl From for SubtitleFile { 110 | fn from(f: ssa::SsaFile) -> SubtitleFile { 111 | SubtitleFile::SubStationAlpha(f) 112 | } 113 | } 114 | 115 | impl From for SubtitleFile { 116 | fn from(f: idx::IdxFile) -> SubtitleFile { 117 | SubtitleFile::VobSubIdxFile(f) 118 | } 119 | } 120 | 121 | impl From for SubtitleFile { 122 | fn from(f: vobsub::VobFile) -> SubtitleFile { 123 | SubtitleFile::VobSubSubFile(f) 124 | } 125 | } 126 | 127 | impl From for SubtitleFile { 128 | fn from(f: microdvd::MdvdFile) -> SubtitleFile { 129 | SubtitleFile::MicroDVDFile(f) 130 | } 131 | } 132 | 133 | impl SubtitleFormat { 134 | /// Get a descriptive string for the format like `".srt (SubRip)"`. 135 | pub fn get_name(&self) -> &'static str { 136 | match *self { 137 | SubtitleFormat::SubRip => ".srt (SubRip)", 138 | SubtitleFormat::SubStationAlpha => ".ssa (SubStation Alpha)", 139 | SubtitleFormat::VobSubIdx => ".idx (VobSub)", 140 | SubtitleFormat::VobSubSub => ".sub (VobSub)", 141 | SubtitleFormat::MicroDVD => ".sub (MicroDVD)", 142 | } 143 | } 144 | } 145 | 146 | #[test] 147 | fn test_subtitle_format_by_extension() { 148 | // this shows how the input paramter can be crated from scratch 149 | assert_eq!(get_subtitle_format_by_extension(Some(OsStr::new("srt"))), Some(SubtitleFormat::SubRip)); 150 | } 151 | 152 | /// Returns the subtitle format by the file extension. 153 | /// 154 | /// Calling the function with the full file path or simply a `get_subtitle_format_by_extension(Some(OsStr::new("srt")))` 155 | /// both work. Returns `None` if subtitle format could not be recognized. 156 | /// 157 | /// Because the `.sub` file extension is ambiguous (both `MicroDVD` and `VobSub` use that extension) the 158 | /// function will return `None` in that case. Instead, use the content-aware `get_subtitle_format` 159 | /// to handle this case correctly. 160 | /// 161 | /// `Option` is used to simplify handling with `PathBuf::extension()`. 162 | pub fn get_subtitle_format_by_extension(extension: Option<&OsStr>) -> Option { 163 | let _ext_opt: Option<&OsStr> = extension.into(); 164 | 165 | if _ext_opt == Some(OsStr::new("srt")) { 166 | Some(SubtitleFormat::SubRip) 167 | } else if _ext_opt == Some(OsStr::new("ssa")) || _ext_opt == Some(OsStr::new("ass")) { 168 | Some(SubtitleFormat::SubStationAlpha) 169 | } else if _ext_opt == Some(OsStr::new("idx")) { 170 | Some(SubtitleFormat::VobSubIdx) 171 | } else { 172 | None 173 | } 174 | } 175 | 176 | /// Returns true if the file extension is valid for the given subtitle format. 177 | /// 178 | /// `Option` is used to simplify handling with `PathBuf::extension()`. 179 | pub fn is_valid_extension_for_subtitle_format(extension: Option<&OsStr>, format: SubtitleFormat) -> bool { 180 | match format { 181 | SubtitleFormat::SubRip => extension == Some(OsStr::new("srt")), 182 | SubtitleFormat::SubStationAlpha => extension == Some(OsStr::new("ssa")) || extension == Some(OsStr::new("ass")), 183 | SubtitleFormat::VobSubIdx => extension == Some(OsStr::new("idx")), 184 | SubtitleFormat::VobSubSub => extension == Some(OsStr::new("sub")), 185 | SubtitleFormat::MicroDVD => extension == Some(OsStr::new("sub")), 186 | } 187 | } 188 | 189 | /// Returns the subtitle format by the file extension. 190 | /// 191 | /// Works exactly like `get_subtitle_format_by_extension`, but instead of `None` a `UnknownFileFormat` 192 | /// will be returned (for simpler error handling). 193 | /// 194 | /// `Option` is used to simplify handling with `PathBuf::extension()`. 195 | pub fn get_subtitle_format_by_extension_err(extension: Option<&OsStr>) -> Result { 196 | get_subtitle_format_by_extension(extension).ok_or_else(|| ErrorKind::UnknownFileFormat.into()) 197 | } 198 | 199 | /// Returns the subtitle format by the file extension and provided content. 200 | /// 201 | /// Calling the function with the full file path or simply a `get_subtitle_format(".sub", content)` 202 | /// both work. Returns `None` if subtitle format could not be recognized. 203 | /// 204 | /// It works exactly the same as `get_subtitle_format_by_extension` (see documentation), but also handles the `.sub` cases 205 | /// correctly by using the provided content of the file as secondary info. 206 | /// 207 | /// `Option` is used to simplify handling with `PathBuf::extension()`. 208 | pub fn get_subtitle_format(extension: Option<&OsStr>, content: &[u8]) -> Option { 209 | if extension == Some(OsStr::new("sub")) { 210 | // test for VobSub .sub magic number 211 | if content.iter().take(4).cloned().eq([0x00, 0x00, 0x01, 0xba].iter().cloned()) { 212 | Some(SubtitleFormat::VobSubSub) 213 | } else { 214 | Some(SubtitleFormat::MicroDVD) 215 | } 216 | } else { 217 | get_subtitle_format_by_extension(extension) 218 | } 219 | } 220 | 221 | /// Returns the subtitle format by the file extension and provided content. 222 | /// 223 | /// Works exactly like `get_subtitle_format`, but instead of `None` a `UnknownFileFormat` 224 | /// will be returned (for simpler error handling). 225 | pub fn get_subtitle_format_err(extension: Option<&OsStr>, content: &[u8]) -> Result { 226 | get_subtitle_format(extension, content).ok_or_else(|| ErrorKind::UnknownFileFormat.into()) 227 | } 228 | 229 | /// Parse text subtitles, invoking the right parser given by `format`. 230 | /// 231 | /// Returns an `Err(ErrorKind::TextFormatOnly)` if attempted on a binary file format. 232 | /// 233 | /// # Mandatory format specific options 234 | /// 235 | /// See `parse_bytes`. 236 | pub fn parse_str(format: SubtitleFormat, content: &str, fps: f64) -> Result { 237 | match format { 238 | SubtitleFormat::SubRip => Ok(srt::SrtFile::parse(content)?.into()), 239 | SubtitleFormat::SubStationAlpha => Ok(ssa::SsaFile::parse(content)?.into()), 240 | SubtitleFormat::VobSubIdx => Ok(idx::IdxFile::parse(content)?.into()), 241 | SubtitleFormat::VobSubSub => Err(ErrorKind::TextFormatOnly.into()), 242 | SubtitleFormat::MicroDVD => Ok(microdvd::MdvdFile::parse(content, fps)?.into()), 243 | } 244 | } 245 | 246 | /// Helper function for text subtitles for byte-to-text decoding (use `None` for automatic detection). 247 | fn decode_bytes_to_string(content: &[u8], encoding: Option<&'static Encoding>) -> Result { 248 | let det_encoding = match encoding { 249 | Some(encoding) => encoding, 250 | None => { 251 | let (charset, _, _) = detect(content); 252 | let encoding_name = charset2encoding(&charset); 253 | Encoding::for_label_no_replacement(encoding_name.as_bytes()).ok_or(ErrorKind::EncodingDetectionError)? 254 | } 255 | }; 256 | 257 | let (decoded, _, replaced) = det_encoding.decode(content); 258 | if replaced { 259 | Err(Error::from(ErrorKind::DecodingError)) 260 | } else { 261 | Ok(decoded.into_owned()) 262 | } 263 | } 264 | 265 | /// Parse all subtitle formats, invoking the right parser given by `format`. 266 | /// 267 | /// # Mandatory format specific options 268 | /// 269 | /// Some subtitle formats require additional parameters to work as expected. If you want to parse 270 | /// a specific format that has no additional parameters, you can use the `parse` function of 271 | /// the respective `***File` struct. 272 | /// 273 | /// `encoding`: to parse a text-based subtitle format, a character encoding is needed (use `None` for auto-detection by `chardet`) 274 | /// 275 | /// `fps`: this parameter is used for `MicroDVD` `.sub` files. These files do not store timestamps in 276 | /// seconds/minutes/... but in frame numbers. So the timing `0 to 30` means "show subtitle for one second" 277 | /// for a 30fps video, and "show subtitle for half second" for 60fps videos. The parameter specifies how 278 | /// frame numbers are converted into timestamps. 279 | pub fn parse_bytes(format: SubtitleFormat, content: &[u8], encoding: Option<&'static Encoding>, fps: f64) -> Result { 280 | match format { 281 | SubtitleFormat::SubRip => Ok(srt::SrtFile::parse(&decode_bytes_to_string(content, encoding)?)?.into()), 282 | SubtitleFormat::SubStationAlpha => Ok(ssa::SsaFile::parse(&decode_bytes_to_string(content, encoding)?)?.into()), 283 | SubtitleFormat::VobSubIdx => Ok(idx::IdxFile::parse(&decode_bytes_to_string(content, encoding)?)?.into()), 284 | SubtitleFormat::VobSubSub => Ok(vobsub::VobFile::parse(content)?.into()), 285 | SubtitleFormat::MicroDVD => Ok(microdvd::MdvdFile::parse(&decode_bytes_to_string(content, encoding)?, fps)?.into()), 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/formats/srt.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use self::errors::ErrorKind::*; 6 | use self::errors::*; 7 | use crate::{SubtitleEntry, SubtitleFileInterface}; 8 | 9 | use crate::errors::Result as SubtitleParserResult; 10 | use crate::formats::common::*; 11 | use combine::char::{char, string}; 12 | use combine::combinator::{eof, parser as p, skip_many}; 13 | use combine::primitives::Parser; 14 | 15 | use failure::ResultExt; 16 | 17 | use itertools::Itertools; 18 | 19 | use crate::timetypes::{TimePoint, TimeSpan}; 20 | use std::iter::once; 21 | 22 | type Result = std::result::Result; 23 | 24 | /// Errors specific to `.srt`-parsing. 25 | #[allow(missing_docs)] 26 | pub mod errors { 27 | 28 | define_error!(Error, ErrorKind); 29 | 30 | #[derive(PartialEq, Debug, Fail)] 31 | pub enum ErrorKind { 32 | #[fail(display = "expected SubRip index line, found '{}'", line)] 33 | ExpectedIndexLine { line: String }, 34 | 35 | #[fail(display = "expected SubRip timespan line, found '{}'", line)] 36 | ExpectedTimestampLine { line: String }, 37 | 38 | #[fail(display = "parse error at line `{}`", line_num)] 39 | ErrorAtLine { line_num: usize }, 40 | } 41 | } 42 | 43 | /// The parsing works as a finite state machine. These are the states in it. 44 | enum SrtParserState { 45 | // emptyline or index follows 46 | Emptyline, 47 | 48 | /// timing line follows 49 | Index(i64), 50 | 51 | /// dialog or emptyline follows 52 | Timing(i64, TimeSpan), 53 | 54 | /// emptyline follows 55 | Dialog(i64, TimeSpan, Vec), 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | /// Represents a `.srt` file. 60 | pub struct SrtFile { 61 | v: Vec, 62 | } 63 | 64 | #[derive(Debug, Clone)] 65 | /// A complete description of one `SubRip` subtitle line. 66 | struct SrtLine { 67 | /// start and end time of subtitle 68 | timespan: TimeSpan, 69 | 70 | /// index/number of line 71 | index: i64, 72 | 73 | /// the dialog/text lines of the `SrtLine` 74 | texts: Vec, 75 | } 76 | 77 | impl SrtFile { 78 | /// Parse a `.srt` subtitle string to `SrtFile`. 79 | pub fn parse(s: &str) -> SubtitleParserResult { 80 | Ok(Self::parse_file(s).with_context(|_| crate::ErrorKind::ParsingError)?) 81 | } 82 | } 83 | 84 | /// Implements parse functions. 85 | impl SrtFile { 86 | fn parse_file(i: &str) -> Result { 87 | use self::SrtParserState::*; 88 | 89 | let mut result: Vec = Vec::new(); 90 | 91 | // remove utf-8 bom 92 | let (_, s) = split_bom(i); 93 | 94 | let mut state: SrtParserState = Emptyline; // expect emptyline or index 95 | 96 | // the `once("")` is there so no last entry gets ignored 97 | for (line_num, line) in s.lines().chain(once("")).enumerate() { 98 | state = match state { 99 | Emptyline => { 100 | if line.trim().is_empty() { 101 | Emptyline 102 | } else { 103 | Index(Self::parse_index_line(line_num, line)?) 104 | } 105 | } 106 | Index(index) => Timing(index, Self::parse_timespan_line(line_num, line)?), 107 | Timing(index, timespan) => Self::state_expect_dialog(line, &mut result, index, timespan, Vec::new()), 108 | Dialog(index, timespan, texts) => Self::state_expect_dialog(line, &mut result, index, timespan, texts), 109 | }; 110 | } 111 | 112 | Ok(SrtFile { v: result }) 113 | } 114 | 115 | fn state_expect_dialog(line: &str, result: &mut Vec, index: i64, timespan: TimeSpan, mut texts: Vec) -> SrtParserState { 116 | if line.trim().is_empty() { 117 | result.push(SrtLine { 118 | index: index, 119 | timespan: timespan, 120 | texts: texts, 121 | }); 122 | SrtParserState::Emptyline 123 | } else { 124 | texts.push(line.trim().to_string()); 125 | SrtParserState::Dialog(index, timespan, texts) 126 | } 127 | } 128 | 129 | /// Matches a line with a single index. 130 | fn parse_index_line(line_num: usize, s: &str) -> Result { 131 | Ok(s.trim() 132 | .parse::() 133 | .with_context(|_| ExpectedIndexLine { line: s.to_string() }) 134 | .with_context(|_| ErrorAtLine { line_num })?) 135 | } 136 | 137 | /// Matches a `SubRip` timespan like "00:24:45,670 --> 00:24:45,680". 138 | fn parse_timespan_line(line_num: usize, line: &str) -> Result { 139 | // Matches a `SubRip` timestamp like "00:24:45,670" 140 | let timestamp = |s| { 141 | ( 142 | p(number_i64), 143 | char(':'), 144 | p(number_i64), 145 | char(':'), 146 | p(number_i64), 147 | char(','), 148 | p(number_i64), 149 | ) 150 | .map(|t| TimePoint::from_components(t.0, t.2, t.4, t.6)) 151 | .parse_stream(s) 152 | }; 153 | 154 | let result = ( 155 | skip_many(ws()), 156 | p(×tamp), 157 | skip_many(ws()), 158 | string("-->"), 159 | skip_many(ws()), 160 | p(×tamp), 161 | skip_many(ws()), 162 | eof(), 163 | ) 164 | .map(|t| TimeSpan::new(t.1, t.5)) 165 | .parse(line) 166 | .map(|x| x.0) 167 | .map_err(|_| Error::from(ExpectedTimestampLine { line: line.to_string() })) 168 | .with_context(|_| ErrorAtLine { line_num })?; 169 | 170 | Ok(result) 171 | } 172 | } 173 | 174 | impl SubtitleFileInterface for SrtFile { 175 | fn get_subtitle_entries(&self) -> SubtitleParserResult> { 176 | let timings = self 177 | .v 178 | .iter() 179 | .map(|line| SubtitleEntry::new(line.timespan, line.texts.iter().join("\n"))) 180 | .collect(); 181 | 182 | Ok(timings) 183 | } 184 | 185 | fn update_subtitle_entries(&mut self, new_subtitle_entries: &[SubtitleEntry]) -> SubtitleParserResult<()> { 186 | assert_eq!(self.v.len(), new_subtitle_entries.len()); // required by specification of this function 187 | 188 | for (line_ref, new_entry_ref) in self.v.iter_mut().zip(new_subtitle_entries) { 189 | line_ref.timespan = new_entry_ref.timespan; 190 | if let Some(ref text) = new_entry_ref.line { 191 | line_ref.texts = text.lines().map(str::to_string).collect(); 192 | } 193 | } 194 | 195 | Ok(()) 196 | } 197 | 198 | fn to_data(&self) -> SubtitleParserResult> { 199 | let timepoint_to_str = 200 | |t: TimePoint| -> String { format!("{:02}:{:02}:{:02},{:03}", t.hours(), t.mins_comp(), t.secs_comp(), t.msecs_comp()) }; 201 | let line_to_str = |line: &SrtLine| -> String { 202 | format!( 203 | "{}\n{} --> {}\n{}\n\n", 204 | line.index, 205 | timepoint_to_str(line.timespan.start), 206 | timepoint_to_str(line.timespan.end), 207 | line.texts.join("\n") 208 | ) 209 | }; 210 | 211 | Ok(self.v.iter().map(line_to_str).collect::().into_bytes()) 212 | } 213 | } 214 | 215 | impl SrtFile { 216 | /// Creates .srt file from scratch. 217 | pub fn create(v: Vec<(TimeSpan, String)>) -> SubtitleParserResult { 218 | let file_parts = v 219 | .into_iter() 220 | .enumerate() 221 | .map(|(i, (ts, text))| SrtLine { 222 | index: i as i64 + 1, 223 | timespan: ts, 224 | texts: text.lines().map(str::to_string).collect(), 225 | }) 226 | .collect(); 227 | 228 | Ok(SrtFile { v: file_parts }) 229 | } 230 | } 231 | 232 | #[cfg(test)] 233 | mod tests { 234 | #[test] 235 | fn create_srt_test() { 236 | use crate::timetypes::{TimePoint, TimeSpan}; 237 | use crate::SubtitleFileInterface; 238 | 239 | let lines = vec![ 240 | ( 241 | TimeSpan::new(TimePoint::from_msecs(1500), TimePoint::from_msecs(3700)), 242 | "line1".to_string(), 243 | ), 244 | ( 245 | TimeSpan::new(TimePoint::from_msecs(4500), TimePoint::from_msecs(8700)), 246 | "line2".to_string(), 247 | ), 248 | ]; 249 | let file = super::SrtFile::create(lines).unwrap(); 250 | 251 | // generate file 252 | let data_string = String::from_utf8(file.to_data().unwrap()).unwrap(); 253 | let expected = "1\n00:00:01,500 --> 00:00:03,700\nline1\n\n2\n00:00:04,500 --> 00:00:08,700\nline2\n\n".to_string(); 254 | println!("\n{:?}\n{:?}", data_string, expected); 255 | assert_eq!(data_string, expected); 256 | } 257 | } 258 | // TODO: parser tests 259 | -------------------------------------------------------------------------------- /src/formats/ssa.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use crate::{SubtitleEntry, SubtitleFileInterface}; 6 | 7 | use crate::errors::Result as SubtitleParserResult; 8 | use crate::formats::common::*; 9 | use combine::char::*; 10 | use combine::combinator::*; 11 | use combine::primitives::Parser; 12 | 13 | use crate::timetypes::{TimePoint, TimeSpan}; 14 | use failure::ResultExt; 15 | use std::iter::once; 16 | 17 | type Result = std::result::Result; 18 | 19 | use self::errors::Error; 20 | use self::errors::ErrorKind::*; 21 | 22 | // Errors specific to the '.ssa' format. 23 | #[allow(missing_docs)] 24 | pub mod errors { 25 | 26 | define_error!(Error, ErrorKind); 27 | 28 | /// `.ssa`-parser-specific errors 29 | #[derive(PartialEq, Debug, Fail)] 30 | pub enum ErrorKind { 31 | #[fail(display = ".ssa/.ass file did not have a line beginning with `Format: ` in a `[Events]` section")] 32 | SsaFieldsInfoNotFound, 33 | 34 | #[fail(display = "the '{}' field is missing in the field info in line {}", f, line_num)] 35 | SsaMissingField { line_num: usize, f: &'static str }, 36 | 37 | #[fail(display = "the '{}' field is twice in the field info in line {}", f, line_num)] 38 | SsaDuplicateField { line_num: usize, f: &'static str }, 39 | 40 | #[fail(display = "the field info in line {} has to have `Text` as its last field", line_num)] 41 | SsaTextFieldNotLast { line_num: usize }, 42 | 43 | #[fail(display = "the dialog at line {} has incorrect number of fields", line_num)] 44 | SsaIncorrectNumberOfFields { line_num: usize }, 45 | 46 | #[fail(display = "the timepoint `{}` in line {} has wrong format", string, line_num)] 47 | SsaWrongTimepointFormat { line_num: usize, string: String }, 48 | 49 | #[fail(display = "parsing the line `{}` failed because of `{}`", line_num, msg)] 50 | SsaDialogLineParseError { line_num: usize, msg: String }, 51 | 52 | #[fail(display = "parsing the line `{}` failed because of `{}`", line_num, msg)] 53 | SsaLineParseError { line_num: usize, msg: String }, 54 | } 55 | } 56 | /*error_chain! { 57 | errors { 58 | SsaFieldsInfoNotFound { 59 | description(".ssa/.ass file did not have a line beginning with `Format: ` in a `[Events]` section") 60 | } 61 | SsaMissingField(line_num: usize, f: &'static str) { 62 | display("the '{}' field is missing in the field info in line {}", f, line_num) 63 | } 64 | SsaDuplicateField(line_num: usize, f: &'static str) { 65 | display("the '{}' field is twice in the field info in line {}", f, line_num) 66 | } 67 | SsaTextFieldNotLast(line_num: usize) { 68 | display("the field info in line {} has to have `Text` as its last field", line_num) 69 | } 70 | SsaIncorrectNumberOfFields(line_num: usize) { 71 | display("the dialog at line {} has incorrect number of fields", line_num) 72 | } 73 | SsaWrongTimepointFormat(line_num: usize, string: String) { 74 | display("the timepoint `{}` in line {} has wrong format", string, line_num) 75 | } 76 | SsaDialogLineParseError(line_num: usize, msg: String) { 77 | display("parsing the line `{}` failed because of `{}`", line_num, msg) 78 | } 79 | SsaLineParseError(line_num: usize, msg: String) { 80 | display("parsing the line `{}` failed because of `{}`", line_num, msg) 81 | } 82 | } 83 | }*/ 84 | 85 | // //////////////////////////////////////////////////////////////////////////////////////////////// 86 | // SSA field info 87 | 88 | struct SsaFieldsInfo { 89 | start_field_idx: usize, 90 | end_field_idx: usize, 91 | text_field_idx: usize, 92 | num_fields: usize, 93 | } 94 | 95 | impl SsaFieldsInfo { 96 | /// Parses a format line like "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text". 97 | fn new_from_fields_info_line(line_num: usize, s: String) -> Result { 98 | assert!(s.starts_with("Format:")); 99 | let field_info = &s["Format:".len()..]; 100 | let mut start_field_idx: Option = None; 101 | let mut end_field_idx: Option = None; 102 | let mut text_field_idx: Option = None; 103 | 104 | // filter "Start" and "End" and "Text" 105 | let split_iter = field_info.split(','); 106 | let num_fields = split_iter.clone().count(); 107 | for (i, field_name) in split_iter.enumerate() { 108 | let trimmed = field_name.trim(); 109 | if trimmed == "Start" { 110 | if start_field_idx.is_some() { 111 | return Err(SsaDuplicateField { line_num, f: "Start" })?; 112 | } 113 | start_field_idx = Some(i); 114 | } else if trimmed == "End" { 115 | if end_field_idx.is_some() { 116 | return Err(SsaDuplicateField { line_num, f: "End" })?; 117 | } 118 | end_field_idx = Some(i); 119 | } else if trimmed == "Text" { 120 | if text_field_idx.is_some() { 121 | return Err(SsaDuplicateField { line_num, f: "Text" })?; 122 | } 123 | text_field_idx = Some(i); 124 | } 125 | } 126 | 127 | let text_field_idx2 = text_field_idx.ok_or_else(|| Error::from(SsaMissingField { line_num, f: "Text" }))?; 128 | if text_field_idx2 != num_fields - 1 { 129 | return Err(SsaTextFieldNotLast { line_num })?; 130 | } 131 | 132 | Ok(SsaFieldsInfo { 133 | start_field_idx: start_field_idx.ok_or_else(|| Error::from(SsaMissingField { line_num, f: "Start" }))?, 134 | end_field_idx: end_field_idx.ok_or_else(|| Error::from(SsaMissingField { line_num, f: "End" }))?, 135 | text_field_idx: text_field_idx2, 136 | num_fields: num_fields, 137 | }) 138 | } 139 | } 140 | 141 | // //////////////////////////////////////////////////////////////////////////////////////////////// 142 | // SSA parser 143 | 144 | impl SsaFile { 145 | /// Parse a `.ssa` subtitle string to `SsaFile`. 146 | pub fn parse(s: &str) -> SubtitleParserResult { 147 | Ok(Self::parse_inner(s.to_string()).with_context(|_| crate::ErrorKind::ParsingError)?) 148 | } 149 | } 150 | 151 | /// Implement parser helper functions. 152 | impl SsaFile { 153 | /// Parses a whole `.ssa` file from string. 154 | fn parse_inner(i: String) -> Result { 155 | let mut file_parts = Vec::new(); 156 | let (bom, s) = split_bom(&i); 157 | file_parts.push(SsaFilePart::Filler(bom.to_string())); 158 | 159 | // first we need to find and parse the format line, which then dictates how to parse the file 160 | let (line_num, field_info_line) = Self::get_format_info(s)?; 161 | let fields_info = SsaFieldsInfo::new_from_fields_info_line(line_num, field_info_line)?; 162 | 163 | // parse the dialog lines with the given format 164 | file_parts.append(&mut Self::parse_dialog_lines(&fields_info, s)?); 165 | Ok(SsaFile::new(file_parts)) 166 | } 167 | 168 | /// Searches and parses a format line like "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text". 169 | fn get_format_info(s: &str) -> Result<(usize, String)> { 170 | let mut section_opt = None; 171 | for (line_num, line) in s.lines().enumerate() { 172 | // parse section headers like `[Events]` 173 | let trimmed_line = line.trim(); 174 | if trimmed_line.starts_with('[') && trimmed_line.ends_with(']') { 175 | section_opt = Some(&trimmed_line[1..trimmed_line.len() - 1]); 176 | } 177 | 178 | // most sections have a format line, but we only want the one for the subtitle events 179 | if section_opt != Some("Events") { 180 | continue; 181 | } 182 | if !line.trim().starts_with("Format:") { 183 | continue; 184 | } 185 | return Ok((line_num, line.to_string())); 186 | } 187 | 188 | Err(SsaFieldsInfoNotFound.into()) 189 | } 190 | 191 | /// Filters file for lines like this and parses them: 192 | /// 193 | /// ```text 194 | /// "Dialogue: 1,0:22:43.52,0:22:46.22,ED-Romaji,,0,0,0,,{\fad(150,150)\blur0.5\bord1}some text" 195 | /// ``` 196 | fn parse_dialog_lines(fields_info: &SsaFieldsInfo, s: &str) -> Result> { 197 | let mut result = Vec::new(); 198 | let mut section_opt: Option = None; 199 | 200 | for (line_num, (line, newl)) in get_lines_non_destructive(s).into_iter().enumerate() { 201 | let trimmed_line = line.trim().to_string(); 202 | 203 | // parse section headers like `[Events]` 204 | if trimmed_line.starts_with('[') && trimmed_line.ends_with(']') { 205 | section_opt = Some(trimmed_line[1..trimmed_line.len() - 1].to_string()); 206 | result.push(SsaFilePart::Filler(line)); 207 | result.push(SsaFilePart::Filler("\n".to_string())); 208 | continue; 209 | } 210 | 211 | if section_opt.is_none() || section_opt.iter().any(|s| s != "Events") || !trimmed_line.starts_with("Dialogue:") { 212 | result.push(SsaFilePart::Filler(line)); 213 | result.push(SsaFilePart::Filler("\n".to_string())); 214 | continue; 215 | } 216 | 217 | result.append(&mut Self::parse_dialog_line(line_num, line.as_str(), fields_info)?); 218 | result.push(SsaFilePart::Filler(newl)); 219 | } 220 | 221 | Ok(result) 222 | } 223 | 224 | /// Parse lines like: 225 | /// 226 | /// ```text 227 | /// "Dialogue: 1,0:22:43.52,0:22:46.22,ED-Romaji,,0,0,0,,{\fad(150,150)\blur0.5\bord1}some text" 228 | /// ``` 229 | fn parse_dialog_line(line_num: usize, line: &str, fields_info: &SsaFieldsInfo) -> Result> { 230 | let parts_res = ( 231 | many(ws()), 232 | string("Dialogue:"), 233 | many(ws()), 234 | count(fields_info.num_fields - 1, (many(none_of(once(','))), token(','))), 235 | many(r#try(any())), 236 | ) 237 | .map( 238 | |(ws1, dl, ws2, v, text): (String, &str, String, Vec<(String, char)>, String)| -> Result> { 239 | let mut result: Vec = Vec::new(); 240 | result.push(SsaFilePart::Filler(ws1)); 241 | result.push(SsaFilePart::Filler(dl.to_string())); 242 | result.push(SsaFilePart::Filler(ws2.to_string())); 243 | result.append(&mut Self::parse_fields(line_num, fields_info, v)?); 244 | result.push(SsaFilePart::Text(text)); 245 | Ok(result) 246 | }, 247 | ) 248 | .parse(line); 249 | 250 | match parts_res { 251 | // Ok() means that parsing succeded, but the "map" function might created an SSA error 252 | Ok((parts, _)) => Ok(parts?), 253 | Err(e) => Err(SsaDialogLineParseError { 254 | line_num, 255 | msg: parse_error_to_string(e), 256 | } 257 | .into()), 258 | } 259 | } 260 | 261 | /// Parses an array of fields with the "fields info". 262 | /// 263 | /// The fields (comma seperated information) as an array like 264 | // `vec!["1", "0:22:43.52", "0:22:46.22", "ED-Romaji", "", "0", "0", "0", "", "{\fad(150,150)\blur0.5\bord1}some text"]`. 265 | fn parse_fields(line_num: usize, fields_info: &SsaFieldsInfo, v: Vec<(String, char)>) -> Result> { 266 | let extract_file_parts_closure = |(i, (field, sep_char)): (_, (String, char))| -> Result> { 267 | let (begin, field, end) = trim_non_destructive(&field); 268 | 269 | let part = if i == fields_info.start_field_idx { 270 | SsaFilePart::TimespanStart(Self::parse_timepoint(line_num, &field)?) 271 | } else if i == fields_info.end_field_idx { 272 | SsaFilePart::TimespanEnd(Self::parse_timepoint(line_num, &field)?) 273 | } else if i == fields_info.text_field_idx { 274 | SsaFilePart::Text(field.to_string()) 275 | } else { 276 | SsaFilePart::Filler(field.to_string()) 277 | }; 278 | 279 | Ok(vec![ 280 | SsaFilePart::Filler(begin), 281 | part, 282 | SsaFilePart::Filler(end), 283 | SsaFilePart::Filler(sep_char.to_string()), 284 | ]) 285 | }; 286 | 287 | let result = v 288 | .into_iter() 289 | .enumerate() 290 | .map(extract_file_parts_closure) 291 | .collect::>>>()? 292 | .into_iter() 293 | .flat_map(|part| part) 294 | .collect(); 295 | Ok(result) 296 | } 297 | 298 | /// Something like "0:19:41.99" 299 | fn parse_timepoint(line_num: usize, s: &str) -> Result { 300 | let parse_res = ( 301 | parser(number_i64), 302 | token(':'), 303 | parser(number_i64), 304 | token(':'), 305 | parser(number_i64), 306 | or(token('.'), token(':')), 307 | parser(number_i64), 308 | eof(), 309 | ) 310 | .map(|(h, _, mm, _, ss, _, ms, _)| TimePoint::from_components(h, mm, ss, ms * 10)) 311 | .parse(s); 312 | match parse_res { 313 | Ok(res) => Ok(res.0), 314 | Err(e) => Err(SsaWrongTimepointFormat { 315 | line_num, 316 | string: parse_error_to_string(e), 317 | } 318 | .into()), 319 | } 320 | } 321 | } 322 | 323 | // //////////////////////////////////////////////////////////////////////////////////////////////// 324 | // SSA file parts 325 | 326 | #[derive(Debug, Clone)] 327 | enum SsaFilePart { 328 | /// Spaces, field information, comments, unimportant fields, ... 329 | Filler(String), 330 | 331 | /// Timespan start of a dialogue line 332 | TimespanStart(TimePoint), 333 | 334 | /// Timespan end of a dialogue line 335 | TimespanEnd(TimePoint), 336 | 337 | /// Dialog lines 338 | Text(String), 339 | } 340 | 341 | // //////////////////////////////////////////////////////////////////////////////////////////////// 342 | // SSA file 343 | 344 | /// Represents a reconstructable `.ssa`/`.ass` file. 345 | /// 346 | /// All unimportant information (for this project) are saved into `SsaFilePart::Filler(...)`, so 347 | /// a timespan-altered file still has the same field etc. 348 | #[derive(Debug, Clone)] 349 | pub struct SsaFile { 350 | v: Vec, 351 | } 352 | 353 | impl SsaFile { 354 | fn new(v: Vec) -> SsaFile { 355 | // cleans up multiple fillers after another 356 | let new_file_parts = dedup_string_parts(v, |part: &mut SsaFilePart| match *part { 357 | SsaFilePart::Filler(ref mut text) => Some(text), 358 | _ => None, 359 | }); 360 | 361 | SsaFile { v: new_file_parts } 362 | } 363 | 364 | /// This function filters out all start times and end times, and returns them ordered 365 | /// (="(start, end, dialog)") so they can be easily read or written to. 366 | /// 367 | /// TODO: implement a single version that takes both `&mut` and `&` (dependent on HKT). 368 | fn get_subtitle_entries_mut<'a>(&'a mut self) -> Vec<(&'a mut TimePoint, &'a mut TimePoint, &'a mut String)> { 369 | let mut startpoint_buffer: Option<&'a mut TimePoint> = None; 370 | let mut endpoint_buffer: Option<&'a mut TimePoint> = None; 371 | 372 | // the extra block satisfies the borrow checker 373 | let timings: Vec<_> = { 374 | let filter_map_closure = |part: &'a mut SsaFilePart| -> Option<(&'a mut TimePoint, &'a mut TimePoint, &'a mut String)> { 375 | use self::SsaFilePart::*; 376 | match *part { 377 | TimespanStart(ref mut start) => { 378 | assert_eq!(startpoint_buffer, None); // parser should have ensured that no two consecutive SSA start times exist 379 | startpoint_buffer = Some(start); 380 | None 381 | } 382 | TimespanEnd(ref mut end) => { 383 | assert_eq!(endpoint_buffer, None); // parser should have ensured that no two consecutive SSA end times exist 384 | endpoint_buffer = Some(end); 385 | None 386 | } 387 | Text(ref mut text) => { 388 | // reset the timepoint buffers 389 | let snatched_startpoint_buffer = startpoint_buffer.take(); 390 | let snatched_endpoint_buffer = endpoint_buffer.take(); 391 | 392 | let start = snatched_startpoint_buffer.expect("SSA parser should have ensured that every line has a startpoint"); 393 | let end = snatched_endpoint_buffer.expect("SSA parser should have ensured that every line has a endpoint"); 394 | 395 | Some((start, end, text)) 396 | } 397 | Filler(_) => None, 398 | } 399 | }; 400 | 401 | self.v.iter_mut().filter_map(filter_map_closure).collect() 402 | }; 403 | 404 | // every timespan should now consist of a beginning and a end (this should be ensured by parser) 405 | assert_eq!(startpoint_buffer, None); 406 | assert_eq!(endpoint_buffer, None); 407 | 408 | timings 409 | } 410 | } 411 | 412 | impl SubtitleFileInterface for SsaFile { 413 | fn get_subtitle_entries(&self) -> SubtitleParserResult> { 414 | // it's unfortunate we have to clone the file before using 415 | // `get_subtitle_entries_mut()`, but otherwise we'd have to copy the` 416 | // `get_subtitle_entries_mut()` and create a non-mut-reference version 417 | // of it (much code duplication); I think a `clone` in this 418 | // not-time-critical code is acceptable, and after HKT become 419 | // available, this can be solved much nicer. 420 | let mut new_file = self.clone(); 421 | let timings = new_file 422 | .get_subtitle_entries_mut() 423 | .into_iter() 424 | .map(|(&mut start, &mut end, text)| SubtitleEntry::new(TimeSpan::new(start, end), text.clone())) 425 | .collect(); 426 | 427 | Ok(timings) 428 | } 429 | 430 | fn update_subtitle_entries(&mut self, new_subtitle_entries: &[SubtitleEntry]) -> SubtitleParserResult<()> { 431 | let subtitle_entries = self.get_subtitle_entries_mut(); 432 | assert_eq!(subtitle_entries.len(), new_subtitle_entries.len()); // required by specification of this function 433 | 434 | for ((start_ref, end_ref, text_ref), new_entry_ref) in subtitle_entries.into_iter().zip(new_subtitle_entries) { 435 | *start_ref = new_entry_ref.timespan.start; 436 | *end_ref = new_entry_ref.timespan.end; 437 | if let Some(ref text) = new_entry_ref.line { 438 | *text_ref = text.clone(); 439 | } 440 | } 441 | 442 | Ok(()) 443 | } 444 | 445 | fn to_data(&self) -> SubtitleParserResult> { 446 | // timing to string like "0:00:22.21" 447 | let fn_timing_to_string = |t: TimePoint| { 448 | let p = if t.msecs() < 0 { -t } else { t }; 449 | format!( 450 | "{}{}:{:02}:{:02}.{:02}", 451 | if t.msecs() < 0 { "-" } else { "" }, 452 | p.hours(), 453 | p.mins_comp(), 454 | p.secs_comp(), 455 | p.csecs_comp() 456 | ) 457 | }; 458 | 459 | let fn_file_part_to_string = |part: &SsaFilePart| { 460 | use self::SsaFilePart::*; 461 | match *part { 462 | Filler(ref t) | Text(ref t) => t.clone(), 463 | TimespanStart(start) => fn_timing_to_string(start), 464 | TimespanEnd(end) => fn_timing_to_string(end), 465 | } 466 | }; 467 | 468 | let result: String = self.v.iter().map(fn_file_part_to_string).collect(); 469 | 470 | Ok(result.into_bytes()) 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/formats/vobsub.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use self::errors::*; 6 | use crate::errors::Result as SubtitleParserResult; 7 | use crate::timetypes::{TimePoint, TimeSpan}; 8 | use crate::{SubtitleEntry, SubtitleFileInterface, SubtitleFormat}; 9 | use failure::ResultExt; 10 | 11 | use vobsub; 12 | 13 | /// `.sub` `VobSub`-parser-specific errors 14 | #[allow(missing_docs)] 15 | pub mod errors { 16 | use vobsub; 17 | 18 | define_error!(Error, ErrorKind); 19 | 20 | #[derive(Debug, Fail)] 21 | pub enum ErrorKind { 22 | // TODO: Vobsub-ErrorKind display 23 | /// Since `vobsub::Error` does not implement Sync. We cannot use #[cause] for it. 24 | VobSubError { cause: vobsub::ErrorKind }, 25 | } 26 | 27 | impl fmt::Display for ErrorKind { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | ErrorKind::VobSubError { cause } => writeln!(f, "VobSub error: {}", cause), 31 | } 32 | } 33 | } 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | /// Represents a `.sub` (`VobSub`) file. 38 | pub struct VobFile { 39 | /// Saves the file data. 40 | data: Vec, 41 | 42 | /// The (with vobsub) extracted subtitle lines. 43 | lines: Vec, 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | /// Represents a line in a `VobSub` `.sub` file. 48 | struct VobSubSubtitle { 49 | timespan: TimeSpan, 50 | } 51 | 52 | impl VobFile { 53 | /// Parse contents of a `VobSub` `.sub` file to `VobFile`. 54 | pub fn parse(b: &[u8]) -> SubtitleParserResult { 55 | let lines = vobsub::subtitles(b) 56 | .map(|sub_res| -> vobsub::Result { 57 | let sub = sub_res?; 58 | 59 | // only extract the timestamps, discard the big image data 60 | Ok(VobSubSubtitle { 61 | timespan: TimeSpan { 62 | start: TimePoint::from_msecs((sub.start_time() * 1000.0) as i64), 63 | end: TimePoint::from_msecs((sub.end_time() * 1000.0) as i64), 64 | }, 65 | }) 66 | }) 67 | .collect::>>() 68 | .map_err(|e| ErrorKind::VobSubError { 69 | cause: vobsub::ErrorKind::from(e), 70 | }) 71 | .with_context(|_| crate::errors::ErrorKind::ParsingError)?; 72 | 73 | Ok(VobFile { 74 | data: b.to_vec(), 75 | lines: lines, 76 | }) 77 | } 78 | } 79 | 80 | impl SubtitleFileInterface for VobFile { 81 | fn get_subtitle_entries(&self) -> SubtitleParserResult> { 82 | Ok(self 83 | .lines 84 | .iter() 85 | .map(|vsub| SubtitleEntry { 86 | timespan: vsub.timespan, 87 | line: None, 88 | }) 89 | .collect()) 90 | } 91 | 92 | fn update_subtitle_entries(&mut self, _: &[SubtitleEntry]) -> SubtitleParserResult<()> { 93 | Err(crate::errors::ErrorKind::UpdatingEntriesNotSupported { 94 | format: SubtitleFormat::VobSubSub, 95 | } 96 | .into()) 97 | } 98 | 99 | fn to_data(&self) -> SubtitleParserResult> { 100 | Ok(self.data.clone()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | #![deny( 6 | missing_docs, 7 | missing_debug_implementations, 8 | missing_copy_implementations, 9 | trivial_casts, 10 | trivial_numeric_casts, 11 | unsafe_code, 12 | unstable_features, 13 | unused_import_braces, 14 | unused_qualifications 15 | )] 16 | 17 | //! This crate provides a common interface for popular subtitle formats (`.srt`, `.ssa`, `.ass`, `.idx`, `.sub`). 18 | //! 19 | //! Files can be parsed, modified and saved again - some formats can be created from scratch. 20 | //! The focus is on non-destructive parsing, meaning that formatting and other information are preserved 21 | //! if not explicitely changed. 22 | 23 | extern crate combine; 24 | extern crate encoding_rs; 25 | extern crate failure; 26 | extern crate itertools; 27 | extern crate vobsub; 28 | 29 | /// Error-chain generated error types. 30 | #[macro_use] 31 | pub mod errors; 32 | 33 | mod formats; 34 | 35 | /// Types that represent a time point, duration and time span. 36 | pub mod timetypes; 37 | 38 | use errors::*; 39 | pub use formats::idx::IdxFile; 40 | pub use formats::microdvd::MdvdFile; 41 | pub use formats::srt::SrtFile; 42 | pub use formats::ssa::SsaFile; 43 | pub use formats::vobsub::VobFile; 44 | pub use formats::{ 45 | get_subtitle_format, get_subtitle_format_by_extension, get_subtitle_format_by_extension_err, get_subtitle_format_err, 46 | is_valid_extension_for_subtitle_format, parse_bytes, parse_str, 47 | }; 48 | pub use formats::{SubtitleFile, SubtitleFormat}; 49 | use timetypes::TimeSpan; 50 | 51 | /// This trait represents the generic interface for reading and writing subtitle information across all subtitle formats. 52 | /// 53 | /// This trait allows you to read, change and rewrite the subtitle file. 54 | pub trait SubtitleFileInterface { 55 | /// The subtitle entries can be changed by calling `update_subtitle_entries()`. 56 | fn get_subtitle_entries(&self) -> Result>; 57 | 58 | /// Set the entries from the subtitle entries from the `get_subtitle_entries()`. 59 | /// 60 | /// The length of the given input slice should always match the length of the vector length from 61 | /// `get_subtitle_entries()`. This function can not delete/create new entries, but preserves 62 | /// everything else in the file (formatting, authors, ...). 63 | /// 64 | /// If the input entry has `entry.line == None`, the line will not be overwritten. 65 | /// 66 | /// Be aware that .idx files cannot save time_spans_ (a subtitle will be shown between two 67 | /// consecutive timepoints/there are no separate starts and ends) - so the timepoint will be set 68 | /// to the start of the corresponding input-timespan. 69 | fn update_subtitle_entries(&mut self, i: &[SubtitleEntry]) -> Result<()>; 70 | 71 | /// Returns a byte-stream in the respective format (.ssa, .srt, etc.) with the 72 | /// (probably) altered information. 73 | fn to_data(&self) -> Result>; 74 | } 75 | 76 | /// The data which can be read from/written to a subtitle file. 77 | #[derive(Debug)] 78 | pub struct SubtitleEntry { 79 | /// The duration for which the current subtitle will be shown. 80 | pub timespan: TimeSpan, 81 | 82 | // TODO: to Vec 83 | /// The text which will be shown in this subtitle. Be aware that 84 | /// for example VobSub files (and any other image based format) 85 | /// will have `None` as value. 86 | pub line: Option, 87 | } 88 | 89 | impl SubtitleEntry { 90 | /// Create subtitle entry with text. 91 | fn new(timespan: TimeSpan, line: String) -> SubtitleEntry { 92 | SubtitleEntry { 93 | timespan: timespan, 94 | line: Some(line), 95 | } 96 | } 97 | } 98 | 99 | impl From for SubtitleEntry { 100 | fn from(f: TimeSpan) -> SubtitleEntry { 101 | SubtitleEntry { timespan: f, line: None } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/timetypes.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 6 | use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; 7 | 8 | /// Represents a timepoint (e.g. start timepoint of a subtitle line). 9 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 10 | struct Timing(i64 /* number of milliseconds */); 11 | 12 | /// The internal timing in `TimePoint` and `TimeDelta` (with all necessary functions and nice Debug information, etc.). 13 | impl Timing { 14 | fn from_components(hours: i64, mins: i64, secs: i64, ms: i64) -> Timing { 15 | Timing(ms + 1000 * (secs + 60 * (mins + 60 * hours))) 16 | } 17 | 18 | fn from_msecs(ms: i64) -> Timing { 19 | Timing(ms) 20 | } 21 | 22 | fn from_csecs(cs: i64) -> Timing { 23 | Timing(cs * 10) 24 | } 25 | 26 | fn from_secs(s: i64) -> Timing { 27 | Timing(s * 1000) 28 | } 29 | 30 | fn from_mins(mins: i64) -> Timing { 31 | Timing(mins * 1000 * 60) 32 | } 33 | 34 | fn from_hours(h: i64) -> Timing { 35 | Timing(h * 1000 * 60 * 60) 36 | } 37 | 38 | fn msecs(&self) -> i64 { 39 | self.0 40 | } 41 | 42 | fn csecs(&self) -> i64 { 43 | self.0 / 10 44 | } 45 | 46 | fn secs(&self) -> i64 { 47 | self.0 / 1000 48 | } 49 | 50 | fn secs_f64(&self) -> f64 { 51 | self.0 as f64 / 1000.0 52 | } 53 | 54 | fn mins(&self) -> i64 { 55 | self.0 / (60 * 1000) 56 | } 57 | 58 | fn hours(&self) -> i64 { 59 | self.0 / (60 * 60 * 1000) 60 | } 61 | 62 | fn mins_comp(&self) -> i64 { 63 | self.mins() % 60 64 | } 65 | 66 | fn secs_comp(&self) -> i64 { 67 | self.secs() % 60 68 | } 69 | 70 | fn csecs_comp(&self) -> i64 { 71 | self.csecs() % 100 72 | } 73 | 74 | fn msecs_comp(&self) -> i64 { 75 | self.msecs() % 1000 76 | } 77 | 78 | fn is_negative(&self) -> bool { 79 | self.0 < 0 80 | } 81 | } 82 | 83 | impl Debug for Timing { 84 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 85 | write!(f, "Timing({})", self.to_string()) 86 | } 87 | } 88 | 89 | impl Display for Timing { 90 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 91 | let t = if self.0 < 0 { -*self } else { *self }; 92 | write!( 93 | f, 94 | "{}{}:{:02}:{:02}.{:03}", 95 | if self.0 < 0 { "-" } else { "" }, 96 | t.hours(), 97 | t.mins_comp(), 98 | t.secs_comp(), 99 | t.msecs_comp() 100 | ) 101 | } 102 | } 103 | 104 | impl Add for Timing { 105 | type Output = Timing; 106 | fn add(self, rhs: Timing) -> Timing { 107 | Timing(self.0 + rhs.0) 108 | } 109 | } 110 | 111 | impl Sub for Timing { 112 | type Output = Timing; 113 | fn sub(self, rhs: Timing) -> Timing { 114 | Timing(self.0 - rhs.0) 115 | } 116 | } 117 | 118 | impl AddAssign for Timing { 119 | fn add_assign(&mut self, r: Timing) { 120 | self.0 += r.0; 121 | } 122 | } 123 | 124 | impl SubAssign for Timing { 125 | fn sub_assign(&mut self, r: Timing) { 126 | self.0 += r.0; 127 | } 128 | } 129 | 130 | impl Neg for Timing { 131 | type Output = Timing; 132 | fn neg(self) -> Timing { 133 | Timing(-self.0) 134 | } 135 | } 136 | 137 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] 138 | /// Represents a time point like the start time of a subtitle entry. 139 | pub struct TimePoint { 140 | /// The internal timing (with all necessary functions and nice Debug information, etc.). 141 | intern: Timing, 142 | } 143 | 144 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] 145 | /// Represents a duration between two `TimePoints`. 146 | pub struct TimeDelta { 147 | /// The internal timing (with all necessary functions and nice Debug information, etc.). 148 | intern: Timing, 149 | } 150 | 151 | macro_rules! create_time_type { 152 | ($i:ident) => { 153 | impl $i { 154 | fn new(t: Timing) -> $i { 155 | $i { intern: t } 156 | } 157 | 158 | /// Create this time type from all time components. 159 | /// 160 | /// The components can be negative and/or exceed the its natural limits without error. 161 | /// For example `from_components(0, 0, 3, -2000)` is the same as `from_components(0, 0, 1, 0)`. 162 | pub fn from_components(hours: i64, mins: i64, secs: i64, ms: i64) -> $i { 163 | Self::new(Timing::from_components(hours, mins, secs, ms)) 164 | } 165 | 166 | /// Create the time type from a given number of milliseconds. 167 | pub fn from_msecs(ms: i64) -> $i { 168 | Self::new(Timing::from_msecs(ms)) 169 | } 170 | 171 | /// Create the time type from a given number of hundreth seconds (10 milliseconds). 172 | pub fn from_csecs(ms: i64) -> $i { 173 | Self::new(Timing::from_csecs(ms)) 174 | } 175 | 176 | /// Create the time type with a given number of seconds. 177 | pub fn from_secs(ms: i64) -> $i { 178 | Self::new(Timing::from_secs(ms)) 179 | } 180 | 181 | /// Create the time type with a given number of minutes. 182 | pub fn from_mins(mins: i64) -> $i { 183 | Self::new(Timing::from_mins(mins)) 184 | } 185 | 186 | /// Create the time type with a given number of hours. 187 | pub fn from_hours(mins: i64) -> $i { 188 | Self::new(Timing::from_hours(mins)) 189 | } 190 | 191 | /// Get the total number of milliseconds. 192 | pub fn msecs(&self) -> i64 { 193 | self.intern.msecs() 194 | } 195 | 196 | /// Get the total number of hundreth seconds. 197 | pub fn csecs(&self) -> i64 { 198 | self.intern.csecs() 199 | } 200 | 201 | /// Get the total number of seconds. 202 | pub fn secs(&self) -> i64 { 203 | self.intern.secs() 204 | } 205 | 206 | /// Get the total number of seconds. 207 | pub fn secs_f64(&self) -> f64 { 208 | self.intern.secs_f64() 209 | } 210 | 211 | /// Get the total number of seconds. 212 | pub fn mins(&self) -> i64 { 213 | self.intern.mins() 214 | } 215 | 216 | /// Get the total number of hours. 217 | pub fn hours(&self) -> i64 { 218 | self.intern.hours() 219 | } 220 | 221 | /// Get the milliseconds component in a range of [0, 999]. 222 | pub fn msecs_comp(&self) -> i64 { 223 | self.intern.msecs_comp() 224 | } 225 | 226 | /// Get the hundreths seconds component in a range of [0, 99]. 227 | pub fn csecs_comp(&self) -> i64 { 228 | self.intern.csecs_comp() 229 | } 230 | 231 | /// Get the seconds component in a range of [0, 59]. 232 | pub fn secs_comp(&self) -> i64 { 233 | self.intern.secs_comp() 234 | } 235 | 236 | /// Get the minute component in a range of [0, 59]. 237 | pub fn mins_comp(&self) -> i64 { 238 | self.intern.mins_comp() 239 | } 240 | 241 | /// Return `true` if the represented time is negative. 242 | pub fn is_negative(&self) -> bool { 243 | self.intern.is_negative() 244 | } 245 | 246 | /// Return the absolute value of the current time. 247 | pub fn abs(&self) -> $i { 248 | if self.is_negative() { 249 | -*self 250 | } else { 251 | *self 252 | } 253 | } 254 | } 255 | 256 | impl Neg for $i { 257 | type Output = $i; 258 | fn neg(self) -> $i { 259 | $i::new(-self.intern) 260 | } 261 | } 262 | 263 | impl Display for $i { 264 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 265 | write!(f, "{}", self.intern) 266 | } 267 | } 268 | }; 269 | } 270 | 271 | create_time_type! {TimePoint} 272 | create_time_type! {TimeDelta} 273 | 274 | macro_rules! impl_add { 275 | ($a:ty, $b:ty, $output:ident) => { 276 | impl Add<$b> for $a { 277 | type Output = $output; 278 | fn add(self, rhs: $b) -> $output { 279 | $output::new(self.intern + rhs.intern) 280 | } 281 | } 282 | }; 283 | } 284 | 285 | macro_rules! impl_sub { 286 | ($a:ty, $b:ty, $output:ident) => { 287 | impl Sub<$b> for $a { 288 | type Output = $output; 289 | fn sub(self, rhs: $b) -> $output { 290 | $output::new(self.intern - rhs.intern) 291 | } 292 | } 293 | }; 294 | } 295 | 296 | macro_rules! impl_add_assign { 297 | ($a:ty, $b:ty) => { 298 | impl AddAssign<$b> for $a { 299 | fn add_assign(&mut self, r: $b) { 300 | self.intern += r.intern; 301 | } 302 | } 303 | }; 304 | } 305 | 306 | macro_rules! impl_sub_assign { 307 | ($a:ty, $b:ty) => { 308 | impl SubAssign<$b> for $a { 309 | fn sub_assign(&mut self, r: $b) { 310 | self.intern -= r.intern; 311 | } 312 | } 313 | }; 314 | } 315 | 316 | impl_add!(TimeDelta, TimeDelta, TimeDelta); 317 | impl_add!(TimePoint, TimeDelta, TimePoint); 318 | impl_add!(TimeDelta, TimePoint, TimePoint); 319 | 320 | impl_sub!(TimeDelta, TimeDelta, TimeDelta); 321 | impl_sub!(TimePoint, TimePoint, TimeDelta); 322 | impl_sub!(TimePoint, TimeDelta, TimePoint); 323 | impl_sub!(TimeDelta, TimePoint, TimePoint); 324 | 325 | impl_add_assign!(TimeDelta, TimeDelta); 326 | impl_add_assign!(TimePoint, TimeDelta); 327 | 328 | impl_sub_assign!(TimeDelta, TimeDelta); 329 | impl_sub_assign!(TimePoint, TimeDelta); 330 | 331 | /// A time span (e.g. time in which a subtitle is shown). 332 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 333 | pub struct TimeSpan { 334 | /// Start of the time span. 335 | pub start: TimePoint, 336 | 337 | /// End of the time span. 338 | pub end: TimePoint, 339 | } 340 | 341 | impl TimeSpan { 342 | /// Constructor of `TimeSpan`s. 343 | pub fn new(start: TimePoint, end: TimePoint) -> TimeSpan { 344 | TimeSpan { start: start, end: end } 345 | } 346 | 347 | /// Get the length of the `TimeSpan` (can be negative). 348 | pub fn len(&self) -> TimeDelta { 349 | self.end - self.start 350 | } 351 | } 352 | 353 | impl Add for TimeSpan { 354 | type Output = TimeSpan; 355 | fn add(self, rhs: TimeDelta) -> TimeSpan { 356 | TimeSpan::new(self.start + rhs, self.end + rhs) 357 | } 358 | } 359 | 360 | impl Sub for TimeSpan { 361 | type Output = TimeSpan; 362 | fn sub(self, rhs: TimeDelta) -> TimeSpan { 363 | TimeSpan::new(self.start - rhs, self.end - rhs) 364 | } 365 | } 366 | 367 | impl AddAssign for TimeSpan { 368 | fn add_assign(&mut self, r: TimeDelta) { 369 | self.start += r; 370 | self.end += r; 371 | } 372 | } 373 | 374 | impl SubAssign for TimeSpan { 375 | fn sub_assign(&mut self, r: TimeDelta) { 376 | self.start -= r; 377 | self.end -= r; 378 | } 379 | } 380 | 381 | #[cfg(test)] 382 | mod tests { 383 | #[test] 384 | fn test_timing_display() { 385 | let t = -super::Timing::from_components(12, 59, 29, 450); 386 | assert_eq!(t.to_string(), "-12:59:29.450".to_string()); 387 | 388 | let t = super::Timing::from_msecs(0); 389 | assert_eq!(t.to_string(), "0:00:00.000".to_string()); 390 | } 391 | } 392 | --------------------------------------------------------------------------------