1extern crate chrono;
2extern crate iso6937;
3extern crate nom;
4
5use std::fmt;
6use std::fs::File;
7use std::io;
8use std::io::prelude::*;
9use std::str;
10
11use codepage_strings::Coding;
12pub mod parser;
13use crate::parser::parse_stl_from_slice;
14pub use crate::parser::ParseError;
15
16#[derive(Debug)]
19pub struct Stl {
20 pub gsi: GsiBlock,
21 pub ttis: Vec<TtiBlock>,
22}
23
24impl fmt::Display for Stl {
25 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26 write!(f, "{}\n{:?}\n", self.gsi, self.ttis)
27 }
28}
29
30pub struct TtiFormat {
31 #[doc = "Justification Code"]
32 pub jc: u8,
33 #[doc = "Vertical Position"]
34 pub vp: u8,
35 #[doc = "Double Height"]
36 pub dh: bool,
37}
38
39impl Stl {
40 pub fn new() -> Stl {
41 Stl {
42 gsi: GsiBlock::new(),
43 ttis: vec![],
44 }
45 }
46
47 pub fn write_to_file(&self, filename: &str) -> Result<(), io::Error> {
48 let mut f = File::create(filename)?;
49 f.write_all(&self.gsi.serialize())?;
50 for tti in self.ttis.iter() {
51 f.write_all(&tti.serialize())?;
52 }
53 Ok(())
54 }
55
56 pub fn add_sub(&mut self, tci: Time, tco: Time, txt: &str, opt: TtiFormat) {
57 if txt.len() > 112 {
58 println!("Warning: sub text is too long!");
60 }
61 self.gsi.tnb += 1; let tti = TtiBlock::new(self.gsi.tnb, tci, tco, txt, opt);
63 self.gsi.tns += 1;
64 self.ttis.push(tti);
65 }
66}
67
68impl Default for Stl {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74pub fn parse_stl_from_file(filename: &str) -> Result<Stl, ParseError> {
75 let mut f = File::open(filename)?;
76 let mut buffer = vec![];
77 f.read_to_end(&mut buffer)?;
78
79 parse_stl_from_slice(&buffer)
80}
81
82struct CodePageDecoder {
83 coding: Coding,
84}
85
86impl CodePageDecoder {
87 pub fn new(codepage: u16) -> Result<Self, ParseError> {
88 Ok(Self {
89 coding: Coding::new(codepage).map_err(|_e| ParseError::CodePageNumber(codepage))?,
90 })
91 }
92
93 fn parse(&self, data: &[u8]) -> Result<String, ParseError> {
94 Ok(self.coding.decode_lossy(data).to_string())
95 }
96}
97
98#[derive(Debug)]
101#[allow(non_camel_case_types)]
102pub enum CodePageNumber {
103 CPN_437,
104 CPN_850,
105 CPN_860,
106 CPN_863,
107 CPN_865,
108}
109
110impl CodePageNumber {
111 fn serialize(&self) -> Vec<u8> {
112 match *self {
113 CodePageNumber::CPN_437 => vec![0x34, 0x33, 0x37],
114 CodePageNumber::CPN_850 => vec![0x38, 0x35, 0x30],
115 CodePageNumber::CPN_860 => vec![0x38, 0x36, 0x30],
116 CodePageNumber::CPN_863 => vec![0x38, 0x36, 0x33],
117 CodePageNumber::CPN_865 => vec![0x38, 0x36, 0x35],
118 }
119 }
120
121 pub(crate) fn from_u16(codepage: u16) -> Result<CodePageNumber, ParseError> {
122 match codepage {
123 437 => Ok(CodePageNumber::CPN_437),
124 850 => Ok(CodePageNumber::CPN_850),
125 860 => Ok(CodePageNumber::CPN_860),
126 863 => Ok(CodePageNumber::CPN_863),
127 865 => Ok(CodePageNumber::CPN_865),
128 _ => Err(ParseError::CodePageNumber(codepage)),
129 }
130 }
131}
132
133#[derive(Debug)]
134pub enum DisplayStandardCode {
135 Blank,
136 OpenSubtitling,
137 Level1Teletext,
138 Level2Teletext,
139}
140
141impl DisplayStandardCode {
142 fn parse(data: u8) -> Result<DisplayStandardCode, ParseError> {
143 match data {
144 0x20 => Ok(DisplayStandardCode::Blank),
145 0x30 => Ok(DisplayStandardCode::OpenSubtitling),
146 0x31 => Ok(DisplayStandardCode::Level1Teletext),
147 0x32 => Ok(DisplayStandardCode::Level2Teletext),
148 _ => Err(ParseError::DisplayStandardCode),
149 }
150 }
151
152 fn serialize(&self) -> u8 {
153 match *self {
154 DisplayStandardCode::Blank => 0x20,
155 DisplayStandardCode::OpenSubtitling => 0x30,
156 DisplayStandardCode::Level1Teletext => 0x31,
157 DisplayStandardCode::Level2Teletext => 0x32,
158 }
159 }
160}
161
162#[derive(Debug)]
163pub enum TimeCodeStatus {
164 NotIntendedForUse,
165 IntendedForUse,
166}
167
168impl TimeCodeStatus {
169 fn parse(data: u8) -> Result<TimeCodeStatus, ParseError> {
170 match data {
171 0x30 => Ok(TimeCodeStatus::NotIntendedForUse),
172 0x31 => Ok(TimeCodeStatus::IntendedForUse),
173 _ => Err(ParseError::TimeCodeStatus),
174 }
175 }
176
177 fn serialize(&self) -> u8 {
178 match *self {
179 TimeCodeStatus::NotIntendedForUse => 0x30,
180 TimeCodeStatus::IntendedForUse => 0x31,
181 }
182 }
183}
184
185#[derive(Debug)]
186pub enum CharacterCodeTable {
187 Latin,
188 LatinCyrillic,
189 LatinArabic,
190 LatinGreek,
191 LatinHebrew,
192}
193
194impl CharacterCodeTable {
195 fn parse(data: &[u8]) -> Result<CharacterCodeTable, ParseError> {
196 if data.len() != 2 {
197 return Err(ParseError::CharacterCodeTable);
198 }
199 if data[0] != 0x30 {
200 return Err(ParseError::CharacterCodeTable);
201 }
202 match data[1] {
203 0x30 => Ok(CharacterCodeTable::Latin),
204 0x31 => Ok(CharacterCodeTable::LatinCyrillic),
205 0x32 => Ok(CharacterCodeTable::LatinArabic),
206 0x33 => Ok(CharacterCodeTable::LatinGreek),
207 0x34 => Ok(CharacterCodeTable::LatinHebrew),
208 _ => Err(ParseError::CharacterCodeTable),
209 }
210 }
211
212 fn serialize(&self) -> Vec<u8> {
213 match *self {
214 CharacterCodeTable::Latin => vec![0x30, 0x30],
215 CharacterCodeTable::LatinCyrillic => vec![0x30, 0x31],
216 CharacterCodeTable::LatinArabic => vec![0x30, 0x32],
217 CharacterCodeTable::LatinGreek => vec![0x30, 0x33],
218 CharacterCodeTable::LatinHebrew => vec![0x30, 0x34],
219 }
220 }
221}
222
223#[derive(Debug)]
224#[allow(non_camel_case_types)]
225pub enum DiskFormatCode {
226 STL25_01,
227 STL30_01,
228}
229
230impl DiskFormatCode {
231 fn parse(data: &str) -> Result<DiskFormatCode, ParseError> {
232 if data == "STL25.01" {
233 Ok(DiskFormatCode::STL25_01)
234 } else if data == "STL30.01" {
235 Ok(DiskFormatCode::STL30_01)
236 } else {
237 Err(ParseError::DiskFormatCode(data.to_string()))
238 }
239 }
240
241 fn serialize(&self) -> Vec<u8> {
242 match *self {
243 DiskFormatCode::STL25_01 => String::from("STL25.01").into_bytes(),
244 DiskFormatCode::STL30_01 => String::from("STL30.01").into_bytes(),
245 }
246 }
247
248 pub fn get_fps(&self) -> usize {
249 match self {
250 DiskFormatCode::STL25_01 => 25,
251 DiskFormatCode::STL30_01 => 30,
252 }
253 }
254}
255
256#[derive(Debug)]
257pub struct GsiBlock {
258 #[doc = "0..2 Code Page Number"]
259 cpn: CodePageNumber,
260 #[doc = "3..10 Disk Format Code"]
261 dfc: DiskFormatCode,
262 #[doc = "11 Display Standard Code"]
263 dsc: DisplayStandardCode,
264 #[doc = "12..13 Character Code Table Number"]
265 cct: CharacterCodeTable,
266 #[doc = "14..15 Language Code"]
267 lc: String,
268 #[doc = "16..47 Original Program Title"]
269 opt: String,
270 #[doc = "48..79 Original Episode Title"]
271 oet: String,
272 #[doc = "80..111 Translated Program Title"]
273 tpt: String,
274 #[doc = "112..143 Translated Episode Title"]
275 tet: String,
276 #[doc = "144..175 Translator's Name"]
277 tn: String,
278 #[doc = "176..207 Translator's Contact Details"]
279 tcd: String,
280 #[doc = "208..223 Subtitle List Reference Code"]
281 slr: String,
282 #[doc = "224..229 Creation Date"]
283 cd: String,
284 #[doc = "230..235 Revision Date"]
285 rd: String,
286 #[doc = "236..237 Revision Number"]
287 rn: String,
288 #[doc = "238..242 Total Number of Text and Timing Blocks"]
289 tnb: u16,
290 #[doc = "243..247 Total Number of Subtitles"]
291 tns: u16,
292 #[doc = "248..250 Total Number of Subtitle Groups"]
293 tng: u16,
294 #[doc = "251..252 Maximum Number of Displayable Characters in a Text Row"]
295 mnc: u16,
296 #[doc = "253..254 Maximum Number of Displayable Rows"]
297 mnr: u16,
298 #[doc = "255 Time Code Status"]
299 tcs: TimeCodeStatus,
300 #[doc = "256..263 Time Code: Start of Programme (format: HHMMSSFF)"]
301 tcp: String,
302 #[doc = "264..271 Time Code: First-in-Cue (format: HHMMSSFF)"]
303 tcf: String,
304 #[doc = "272 Total Number of Disks"]
305 tnd: u8,
306 #[doc = "273 Disk Sequence Number"]
307 dsn: u8,
308 #[doc = "274..276 Country of Origin"]
309 co: String, #[doc = "277..308 Publisher"]
311 pub_: String,
312 #[doc = "309..340 Editor's Name"]
313 en: String,
314 #[doc = "341..372 Editor's Contact Details"]
315 ecd: String,
316 #[doc = "373..447 Spare Bytes"]
317 _spare: String,
318 #[doc = "448..1023 User-Defined Area"]
319 uda: String,
320}
321
322impl GsiBlock {
323 pub fn get_code_page_number(&self) -> &CodePageNumber {
324 &self.cpn
325 }
326 pub fn get_disk_format_code(&self) -> &DiskFormatCode {
327 &self.dfc
328 }
329 pub fn get_display_standard_code(&self) -> &DisplayStandardCode {
330 &self.dsc
331 }
332 pub fn get_character_code_table(&self) -> &CharacterCodeTable {
333 &self.cct
334 }
335 pub fn get_language_code(&self) -> &str {
336 &self.lc
337 }
338 pub fn get_original_program_title(&self) -> &str {
339 &self.opt
340 }
341 pub fn get_original_episode_title(&self) -> &str {
342 &self.oet
343 }
344 pub fn get_translated_program_title(&self) -> &str {
345 &self.tpt
346 }
347 pub fn get_translated_episode_title(&self) -> &str {
348 &self.tet
349 }
350 pub fn get_translators_name(&self) -> &str {
351 &self.tn
352 }
353 pub fn get_translators_contact_details(&self) -> &str {
354 &self.tcd
355 }
356 pub fn get_subtitle_list_reference_code(&self) -> &str {
357 &self.slr
358 }
359 pub fn get_creation_date(&self) -> &str {
360 &self.cd
361 }
362 pub fn get_revision_date(&self) -> &str {
363 &self.rd
364 }
365 pub fn get_revision_number(&self) -> &str {
366 &self.rn
367 }
368 pub fn get_total_number_of_text_and_timing_blocks(&self) -> u16 {
369 self.tnb
370 }
371 pub fn get_total_number_of_subtitles(&self) -> u16 {
372 self.tns
373 }
374 pub fn get_total_number_of_chars_in_row(&self) -> u16 {
375 self.tng
376 }
377 pub fn get_max_number_of_chars_in_row(&self) -> u16 {
378 self.mnc
379 }
380 pub fn get_max_number_of_rows(&self) -> u16 {
381 self.mnr
382 }
383 pub fn get_timecode_status(&self) -> &TimeCodeStatus {
384 &self.tcs
385 }
386 pub fn get_timecode_start_of_program(&self) -> &str {
387 &self.tcp
388 }
389 pub fn get_timecode_first_in_cue(&self) -> &str {
390 &self.tcf
391 }
392 pub fn get_total_number_of_disks(&self) -> u8 {
393 self.tnd
394 }
395 pub fn get_disk_sequence_number(&self) -> u8 {
396 self.dsn
397 }
398 pub fn get_country_of_origin(&self) -> &str {
399 &self.co
400 }
401 pub fn get_publisher(&self) -> &str {
402 &self.pub_
403 }
404 pub fn get_editors_name(&self) -> &str {
405 &self.en
406 }
407 pub fn get_editors_contact_details(&self) -> &str {
408 &self.ecd
409 }
410 pub fn get_user_defined_area(&self) -> &str {
411 &self.uda
412 }
413}
414
415fn push_string(v: &mut Vec<u8>, s: &str, len: usize) {
416 let addendum = s.to_owned().into_bytes();
417 let padding = len - addendum.len();
418 v.extend(addendum.iter().cloned());
419 v.extend(vec![0x20u8; padding]);
420}
421
422impl GsiBlock {
423 pub fn new() -> GsiBlock {
424 let date = chrono::Local::now();
425 let now = date.format("%y%m%d").to_string();
426 GsiBlock {
427 cpn: CodePageNumber::CPN_850,
428 dfc: DiskFormatCode::STL25_01,
429 dsc: DisplayStandardCode::Level1Teletext,
430 cct: CharacterCodeTable::Latin,
431 lc: "0F".to_string(), opt: "".to_string(),
433 oet: "".to_string(),
434 tpt: "".to_string(),
435 tet: "".to_string(),
436 tn: "".to_string(),
437 tcd: "".to_string(),
438 slr: "".to_string(),
439 cd: now.clone(),
440 rd: now,
441 rn: "00".to_string(),
442 tnb: 0,
443 tns: 0,
444 tng: 1, mnc: 40, mnr: 23, tcs: TimeCodeStatus::IntendedForUse,
448 tcp: "00000000".to_string(),
449 tcf: "00000000".to_string(),
450 tnd: 1,
451 dsn: 1,
452 co: "".to_string(),
453 pub_: "".to_string(),
454 en: "".to_string(),
455 ecd: "".to_string(),
456 _spare: "".to_string(),
457 uda: "".to_string(),
458 }
459 }
460
461 fn serialize(&self) -> Vec<u8> {
462 let mut res = Vec::with_capacity(1024);
463 res.extend(self.cpn.serialize());
464 res.extend(self.dfc.serialize().iter().cloned());
465 res.push(self.dsc.serialize());
466 res.extend(self.cct.serialize());
467 push_string(&mut res, &self.lc, 15 - 14 + 1);
469 push_string(&mut res, &self.opt, 47 - 16 + 1);
470 push_string(&mut res, &self.oet, 79 - 48 + 1);
471 push_string(&mut res, &self.tpt, 111 - 80 + 1);
472 push_string(&mut res, &self.tet, 143 - 112 + 1);
473 push_string(&mut res, &self.tn, 175 - 144 + 1);
474 push_string(&mut res, &self.tcd, 207 - 176 + 1);
475 push_string(&mut res, &self.slr, 223 - 208 + 1);
476 push_string(&mut res, &self.cd, 229 - 224 + 1);
477 push_string(&mut res, &self.rd, 235 - 230 + 1);
478 push_string(&mut res, &self.rn, 237 - 236 + 1);
479
480 push_string(&mut res, &format!("{:05}", self.tnb), 242 - 238 + 1);
481 push_string(&mut res, &format!("{:05}", self.tns), 247 - 243 + 1);
482 push_string(&mut res, &format!("{:03}", self.tng), 250 - 248 + 1);
483 push_string(&mut res, &format!("{:02}", self.mnc), 252 - 251 + 1);
484 push_string(&mut res, &format!("{:02}", self.mnr), 254 - 253 + 1);
485
486 res.push(self.tcs.serialize());
487 push_string(&mut res, &self.tcp, 263 - 256 + 1);
488 push_string(&mut res, &self.tcf, 271 - 264 + 1);
489 push_string(&mut res, &format!("{:1}", self.tnd), 1);
490 push_string(&mut res, &format!("{:1}", self.dsn), 1);
491 push_string(&mut res, &self.co, 276 - 274 + 1);
492 push_string(&mut res, &self.pub_, 308 - 277 + 1);
493 push_string(&mut res, &self.en, 340 - 309 + 1);
494 push_string(&mut res, &self.ecd, 372 - 341 + 1);
495 push_string(&mut res, &self._spare, 447 - 373 + 1);
496 push_string(&mut res, &self.uda, 1023 - 448 + 1);
497
498 res
499 }
500}
501
502impl Default for GsiBlock {
503 fn default() -> Self {
504 Self::new()
505 }
506}
507
508impl fmt::Display for GsiBlock {
509 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
510 write!(
511 f,
512 "Program Title: {}\nEpisode Title: {}\ncct:{:?} lc:{}\n",
513 self.opt, self.oet, self.cct, self.lc
514 )
515 }
516}
517
518#[derive(Debug)]
521pub enum CumulativeStatus {
522 NotPartOfASet,
523 FirstInSet,
524 IntermediateInSet,
525 LastInSet,
526}
527
528impl CumulativeStatus {
529 fn parse(d: u8) -> Result<CumulativeStatus, ParseError> {
530 match d {
531 0 => Ok(CumulativeStatus::NotPartOfASet),
532 1 => Ok(CumulativeStatus::FirstInSet),
533 2 => Ok(CumulativeStatus::IntermediateInSet),
534 3 => Ok(CumulativeStatus::LastInSet),
535 _ => Err(ParseError::CumulativeStatus),
536 }
537 }
538
539 fn serialize(&self) -> u8 {
540 match *self {
541 CumulativeStatus::NotPartOfASet => 0,
542 CumulativeStatus::FirstInSet => 1,
543 CumulativeStatus::IntermediateInSet => 2,
544 CumulativeStatus::LastInSet => 3,
545 }
546 }
547}
548
549pub enum Justification {
550 Unchanged,
551 Left,
552 Centered,
553 Right,
554}
555
556#[derive(Debug, PartialEq, Eq)]
557pub struct Time {
558 pub hours: u8,
559 pub minutes: u8,
560 pub seconds: u8,
561 pub frames: u8,
562}
563
564impl Time {
565 fn new(h: u8, m: u8, s: u8, f: u8) -> Time {
566 Time {
567 hours: h,
568 minutes: m,
569 seconds: s,
570 frames: f,
571 }
572 }
573
574 pub fn format_fps(&self, fps: usize) -> String {
575 format!(
576 "{}:{}:{},{}",
577 self.hours,
578 self.minutes,
579 self.seconds,
580 self.frames as usize * 1000 / fps
581 )
582 }
583 fn serialize(&self) -> Vec<u8> {
584 vec![self.hours, self.minutes, self.seconds, self.frames]
585 }
586}
587
588impl fmt::Display for Time {
589 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
590 write!(
591 f,
592 "{}:{}:{}/{})",
593 self.hours, self.minutes, self.seconds, self.frames
594 )
595 }
596}
597
598pub struct TtiBlock {
599 #[doc = "0 Subtitle Group Number. 00h-FFh"]
600 sgn: u8,
601 #[doc = "1..2 Subtitle Number range. 0000h-FFFFh"]
602 sn: u16,
603 #[doc = "3 Extension Block Number. 00h-FFh"]
604 ebn: u8,
605 #[doc = "4 Cumulative Status. 00-03h"]
606 cs: CumulativeStatus,
607 #[doc = "5..8 Time Code In"]
608 tci: Time,
609 #[doc = "9..12 Time Code Out"]
610 tco: Time,
611 #[doc = "13 Vertical Position"]
612 vp: u8,
613 #[doc = "14 Justification Code"]
614 jc: u8,
615 #[doc = "15 Comment Flag"]
616 cf: u8,
617 #[doc = "16..127 Text Field"]
618 tf: Vec<u8>,
619}
620
621impl TtiBlock {
622 pub fn get_subtitle_group_number(&self) -> u8 {
623 self.sgn
624 }
625 pub fn get_subtitle_number_range(&self) -> u16 {
626 self.sn
627 }
628 pub fn get_extension_block_number(&self) -> u8 {
629 self.ebn
630 }
631 pub fn get_cumulative_status(&self) -> &CumulativeStatus {
632 &self.cs
633 }
634 pub fn get_time_code_in(&self) -> &Time {
635 &self.tci
636 }
637 pub fn get_time_code_out(&self) -> &Time {
638 &self.tco
639 }
640 pub fn get_vertical_position(&self) -> u8 {
641 self.vp
642 }
643 pub fn get_justification_code(&self) -> u8 {
644 self.jc
645 }
646 pub fn get_comment_flag(&self) -> u8 {
647 self.cf
648 }
649}
650
651impl TtiBlock {
652 pub fn new(idx: u16, tci: Time, tco: Time, txt: &str, opt: TtiFormat) -> TtiBlock {
653 TtiBlock {
654 sgn: 0,
655 sn: idx,
656 ebn: 0xff,
657 cs: CumulativeStatus::NotPartOfASet,
658 tci,
659 tco,
660 vp: opt.vp,
661 jc: opt.jc,
662 cf: 0,
663 tf: TtiBlock::encode_text(txt, opt.dh),
664 }
665 }
666
667 fn encode_text(txt: &str, dh: bool) -> Vec<u8> {
668 const TF_LENGTH: usize = 112;
669 let text = iso6937::encode(txt);
670 let mut res = Vec::with_capacity(TF_LENGTH);
671 if dh {
672 res.push(0x0d);
673 }
674 res.push(0x0b);
675 res.push(0x0b);
676 res.extend(text);
677
678 let max_size = TF_LENGTH - 3; if res.len() > max_size {
681 println!("!!! subtitle length is too long, truncating!");
682 }
683 res.truncate(max_size);
684 res.push(0x0A);
685 res.push(0x0A);
686 res.push(0x8A);
687 let padding = TF_LENGTH - res.len();
688 res.extend(vec![0x8Fu8; padding]);
689 res
690 }
691
692 pub fn get_text(&self) -> String {
693 let mut result = String::from("");
694 let mut first = 0;
695 for i in 0..self.tf.len() {
696 let c = self.tf[i];
697 if match c {
698 0x0..=0x1f => true, 0x20..=0x7f => false,
700 0x80..=0x9f => true, 0xa0..=0xff => false,
702 } {
703 if first != i {
704 result.push_str(&iso6937::decode(&self.tf[first..i]));
705 }
706 if c == 0x8f {
707 break;
708 } else if c == 0x8a {
709 result.push_str("\r\n");
710 }
711 first = i + 1;
712 }
713 }
714 result
715 }
716
717 #[allow(clippy::vec_init_then_push)]
718 fn serialize(&self) -> Vec<u8> {
719 let mut res = vec![];
720 res.push(self.sgn);
721 res.push((self.sn & 0xff) as u8);
722 res.push((self.sn >> 8) as u8);
723 res.push(self.ebn);
724 res.push(self.cs.serialize());
725 res.extend(self.tci.serialize().iter().cloned());
726 res.extend(self.tco.serialize().iter().cloned());
727 res.push(self.vp);
728 res.push(self.jc);
729 res.push(self.cf);
730 res.extend(self.tf.iter().cloned());
731 res
732 }
733}
734
735impl fmt::Debug for TtiBlock {
736 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
737 write!(
738 f,
739 "\n{}-->{} sgn:{} sn:{} ebn:{} cs:{:?} vp:{} jc:{} cf:{} [{}]",
740 self.tci,
741 self.tco,
742 self.sgn,
743 self.sn,
744 self.ebn,
745 self.cs,
746 self.vp,
747 self.jc,
748 self.cf,
749 self.get_text()
750 )
751 }
752}
753
754impl fmt::Display for TtiBlock {
755 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
756 write!(
757 f,
758 "\n{} {} {} {} {:?} [{}]",
759 self.tci,
760 self.sgn,
761 self.sn,
762 self.ebn,
763 self.cs,
764 self.get_text()
765 )
766 }
767}