1use std::fmt::Error;
2
3pub mod pdf_document;
4pub mod reports;
5pub mod screenplay_document;
6
7pub mod pdf_parser;
8
9#[cfg(feature = "mupdf-basic-parsing")]
10pub mod mupdf_basic_parser;
11
12#[cfg(test)]
13mod tests {
14
15 use crate::{
16 pdf_document::{ElementIndentationsPoints, PDFDocument, TextPosition},
17 pdf_parser::deduce_indentations,
18 screenplay_document::{EnvironmentStrings, SPType, TimeOfDayCollection},
19 };
20
21 use super::*;
22
23 fn _create_pdfline_with_word(
24 text: String,
25 element_indentation: f64,
26 y_height_inches: Option<f64>,
27 ) -> pdf_document::Line {
28 let mut new_word = pdf_document::Word::default();
29
30 if let Some(inches) = y_height_inches {
31 new_word = _create_pdfword(text, element_indentation, y_height_inches);
32 } else {
33 new_word = _create_pdfword(text, element_indentation, None);
34 }
35
36 let new_line: pdf_document::Line = pdf_document::Line {
37 words: vec![new_word],
38 };
39 new_line
40 }
41
42 fn _create_pdfword(
43 text: String,
44 element_indentation: f64,
45 y_height_inches: Option<f64>,
46 ) -> pdf_document::Word {
47 let mut y_height_pts = 0.0;
48 if let Some(inches) = y_height_inches {
49 y_height_pts = 72.0 * inches;
50 } else {
51 y_height_pts = 3.0 * 72.0;
52 }
53
54 let new_word: pdf_document::Word = pdf_document::Word {
55 text: text.clone(),
56 bbox_width: text.len() as f64 * 7.2 as f64,
57 bbox_height: 0.0,
58 position: TextPosition {
59 x: element_indentation,
60 y: y_height_pts,
61 },
62 font_name: None,
63 font_size: 12.0,
64 font_character_width: 7.2,
65 };
66 new_word
67 }
68
69 #[test]
70 fn indent_deduction() {
71 let doc_result =
72 mupdf_basic_parser::get_pdf_obj_from_filepath("test_data/VCR2L.pdf".to_string());
73 if let Ok(doc) = doc_result {
74 let indentations_opt = deduce_indentations(&doc);
75 }
76 }
77
78 #[cfg(feature = "mupdf-basic-parsing")]
79 #[test]
80 fn test_mupdf_line_word_spacing() {
81 use mupdf_basic_parser;
82
83 let custom_indentations = ElementIndentationsInches::us_letter_default();
84 use crate::pdf_document::ElementIndentationsInches;
85 let screenplay_result = mupdf_basic_parser::get_screenplay_doc_from_filepath(
86 "/home/rich/Documents/test_pdfs/VCR2L-2024-04-04.pdf".into(),
89 Some(custom_indentations),
90 None,
91 None,
92 None,
93 );
94 let Ok(screenplay) = screenplay_result else {
95 println!("{:#?}", screenplay_result);
96 panic!();
97 };
98
99 for (pidx, page) in screenplay.pages.iter().enumerate() {
100 if pidx >= 3 {
101 break;
102 }
103 println!("Page: {}", pidx);
104 for line in &page.lines {
105 let mut text_str: String = String::new();
106 for te in &line.text_elements {
107 if te.preceding_whitespace_chars > 0 {
108 for n in 0..te.preceding_whitespace_chars {
109 text_str.push(' ');
110 }
111 }
112 text_str.push_str(&te.text);
113 }
114 println!(
115 " Pre-blank lns: {:>4} | y-pos: {:>6} | type: {:>24} |{}",
116 line.preceding_empty_lines,
117 format!("{:.2}",line.text_elements.iter().nth(0).unwrap().element_position.unwrap().y,),
118 format!("{:?}", line.line_type),
119 text_str
120 )
121 }
122 }
123 }
124
125 #[cfg(feature = "mupdf-basic-parsing")]
126 #[test]
127 fn test_mupdf_parsing() {
128 let start = Instant::now();
129 use crate::reports;
130 use mupdf_basic_parser;
131 use std::time::Instant;
132
133 let custom_indentations = ElementIndentationsInches::us_letter_default();
134 use crate::pdf_document::ElementIndentationsInches;
135 let screenplay_result = mupdf_basic_parser::get_screenplay_doc_from_filepath(
136 "test_data/VCR2L.pdf".into(),
138 Some(custom_indentations),
139 None,
140 None,
141 None,
142 );
143 let Ok(screenplay) = screenplay_result else {
144 println!("{:#?}", screenplay_result);
145 panic!();
146 };
147
148 println!("ALL SCENES:");
154 let scenes_opt = reports::get_all_scenes_ordered(&screenplay);
155 if let Some(scenes) = scenes_opt {
156 for (scn, scene_obj) in scenes {
157 let Some(leaf_location) = &scene_obj.story_locations.last() else {
158 continue;
159 };
160 println!(
161 "SCENE: P:{:<3?}, L:{:<3?} | LOCATION: {:?}",
162 scene_obj.start.page,
163 scene_obj.start.line,
164 reports::get_full_string_for_location_path(&screenplay, &leaf_location)
165 );
166 }
167 println!("");
168 }
169 println!("\n- SCENES PER PAGE:");
170 for (p_idx, page) in screenplay.pages.iter().enumerate() {
171 let Some(scenes_for_page) =
172 reports::get_all_scenes_on_page_by_index(&screenplay, p_idx)
173 else {
174 continue;
175 };
176 println!("--- PAGE: {:?}", p_idx);
177 for (id, scn) in scenes_for_page {
178 for loc in &scn.story_locations {
179 println!(
180 "----- {:?}",
181 reports::get_full_string_for_location_path(&screenplay, &loc)
182 )
183 }
184 }
185 }
186
187 println!("\nALL LOCATIONS:");
190 for (id, location) in &screenplay.locations {
191 println!("LOCATION_ID: {:?}, | LOCATION: {:}", id, location.string);
192 }
193
194 println!("\n-----------LOCATION_HEIRARCHY----------");
195
196 for (id, location) in &screenplay.locations {
197 if location.superlocation.is_none() {
198 println!("\nLOCATION_ID: {:?}, | LOCATION: {:}", id, location.string);
199 let Some(leafs) = reports::get_all_location_leafs(&screenplay, id) else {
200 panic!();
201 };
202 println!("------- LEAFS FOR ROOT");
203 for id in leafs {
204 let Some(leaf) = screenplay.locations.get(id) else {
205 continue;
206 };
207 println!(" ------- : | {:?}", leaf.string);
208 }
209 }
210 }
211
212 for (id, location) in &screenplay.locations {
213 if location.sublocations.is_empty() {
214 println!("LOCATION: {:}", location.string);
215 let Some(root) = reports::get_location_root_for_node(&screenplay, id) else {
216 panic!();
217 };
218 let Some(root_node) = screenplay.locations.get(&root) else {
219 panic!()
220 };
221 println!(
222 "--------- ROOT FOR LEAF: {:?}, | {:?}",
223 root, root_node.string
224 )
225 }
226 }
227
228 println!("");
229
230 for (id, location) in &screenplay.locations {
231 if !location.sublocations.is_empty() {
232 continue;
233 }
234 println!("LOCATION_LEAF: {:?}", location.string);
235 let Some(path_string) = reports::get_full_string_for_location_path(&screenplay, &id)
236 else {
237 continue;
238 };
239 println!("-- FULL PATH FOR LEAF: {:?}\n", path_string);
240 }
241
242 println!("\n-----\n");
243
244 if screenplay.characters.is_empty() {
245 println!("NO CHARACTERS FOUND!");
246 }
247
248 println!("\n- GET CHARACTERS PER SCENE:");
251 let scenes_ordered = reports::get_all_scenes_ordered(&screenplay).unwrap();
252 for (scn_id, scn) in scenes_ordered {
253 let Some(characters) = reports::get_characters_for_scene(&screenplay, scn_id) else {
254 continue;
255 };
256
257 println!(
258 "--- SCENE: P:{:<4?}, L:{:<4}",
259 scn.start.page, scn.start.line
260 );
261 for character in characters {
262 println!("----- {:?}", character.name)
263 }
264 }
265
266 println!("\n- GET CHARACTERS PER LOCATION:");
267 for (loc_id, loc) in &screenplay.locations {
268 let Some(characters) = reports::get_characters_for_location(&screenplay, loc_id) else {
269 println!("NO CHARACTERS FOUND AT LOCATION?!");
270 continue;
271 };
272
273 println!(
274 "--- LOCATION: {:?}",
275 reports::get_full_string_for_location_path(&screenplay, loc_id)
276 );
277
278 for character in characters {
279 println!("----- {:?}", character.name)
280 }
281 }
282
283 println!("\n - GET CHARACTERS PER PAGE:");
284 for (pidx, page) in screenplay.pages.iter().enumerate() {
285 println!("PAGE INDEX: {:?} | NOMINAL PAGE NUMBER: {:?}", pidx, {
286 if let Some(pn) = &page.page_number {
287 pn.to_string()
288 } else {
289 "_".to_string()
290 }
291 });
292 let Some(characters) = reports::get_all_characters_on_page_by_index(&screenplay, pidx)
293 else {
294 println!("No Characters on this page!");
295 continue;
296 };
297 print!("----- ");
298 for character in characters {
299 print!("{:?} | ", character.name);
300 }
301 println!("");
302 }
303
304 println!("\n\n");
305
306 for character in &screenplay.characters {
308 println!(
309 "CHARACTER ID: {:?} | CHARACTER: {:?}",
310 character.id, character.name
311 );
312 let get_all_char_lines_start = Instant::now();
313 let Some(lines) =
314 reports::get_all_lines_of_dialogue_for_character(&screenplay, character)
315 else {
316 continue;
318 };
319 let get_all_char_liens_end = get_all_char_lines_start.elapsed();
320 println!(
321 "TIME TAKEN TO GET ALL DIALOGUE FOR THIS CHARACTER: {:?}",
322 get_all_char_liens_end
323 );
324 println!("LINES OF DIALOGUE FOR CHARACTER: {:?}", lines.len());
325 let mut wordcount: usize = 0;
326 for (coord, line) in lines {
327 let mut line_str = String::new();
328 wordcount += line.text_elements.len();
329 line.text_elements
331 .iter()
332 .map(|te| te.text.clone())
333 .for_each(|ts| {
334 if !line_str.is_empty() {
335 line_str.push(' ');
336 }
337 line_str.push_str(&ts)
338 });
339 }
341 println!("WORDS FOR CHARACTER: {:}", wordcount);
342 }
343
344 for character in &screenplay.characters {
346 let get_scenes_with_char_bench_start = Instant::now();
347 let Some(scenes_with_char_speaking) =
348 reports::get_all_scenes_with_character_speaking(&screenplay, &character)
349 else {
350 continue;
351 };
352 let get_scenes_with_char_bench_end = get_scenes_with_char_bench_start.elapsed();
353
354 println!("CHARACTER: {:?}", character.name);
355 println!(
356 "\n--TIME TAKEN TO GET ALL SCENES WITH THIS CHARACTER: {:?}\n",
357 get_scenes_with_char_bench_end
358 );
359 println!("--ALL SCENES WITH CHARACTER SPEAKING:");
360 for (scn, scene_obj) in &scenes_with_char_speaking {
361 let Some(location) = screenplay
362 .locations
363 .get(scene_obj.story_locations.last().unwrap())
364 else {
365 continue;
366 };
367 print!("------");
368 println!("{:?} | {:?}", scene_obj.start, location.string)
369 }
370 }
371
372 println!("\n- GET LOCATIONS PER CHARACTER:");
375 for character in &screenplay.characters {
376 let Some(locations_per_character) =
377 reports::get_all_locations_with_character_speaking(&screenplay, &character)
378 else {
379 println!("No locations for character?!?!");
380 continue;
381 };
382 println!("-- CHARACTER: {:?}", character.name);
383 for lc in locations_per_character {
384 println!(
385 "----- {:?}",
386 reports::get_full_string_for_location_path(&screenplay, lc)
387 );
388 }
389 }
390
391 println!("\n- GET LOCATIONS PER PAGE:");
392 for page_num in 0..=screenplay.pages.len() - 1 {
393 println!("PAGE: {}", page_num);
394 let Some(locations_on_page) =
395 reports::get_all_locations_on_page_by_index(&screenplay, page_num)
396 else {
397 println!(" NO LOCATIONS ON PAGE????");
398 continue;
399 };
400 for loc in locations_on_page {
401 println!(
402 "----- {:?}",
403 reports::get_full_string_for_location_path(&screenplay, loc)
404 );
405 }
406 }
407
408 println!("\n - GET PAGES FOR LOCATION:");
410 for (l_id, location) in &screenplay.locations {
411 let Some(pages) = reports::get_all_pages_for_location(&screenplay, &l_id) else {
412 continue;
413 };
414 println!("--- LOCATION: {:?}", location.string);
415 for (pidx, _page) in pages {
416 println!("----- {:?}", pidx);
417 }
418 }
419
420 println!("\n - GET PAGES FOR CHARACTER:");
421 for character in &screenplay.characters {
422 println!("--- CHARACTER: {:?}", character.name);
423 let Some(pages) =
424 reports::get_all_pages_for_character_speaking(&screenplay, &character)
425 else {
426 continue;
428 };
429 for (pidx, _page) in pages {
430 println!("----- {:?}", pidx)
431 }
432 }
433
434 let print_filtered_dialogue = false;
437
438 println!("\nLOCATION FILTERED CHARACTER DIALOGUE LINES:");
439 for (location_id, location) in &screenplay.locations {
440 if !print_filtered_dialogue {
441 break;
442 }
443 println!("\nLOCATION: {:?}", location.string);
444 for character in &screenplay.characters {
445 use crate::screenplay_document::ScreenplayCoordinate;
446
447 println!("\n-- CHARACTER: {:?}", character.name);
448 let Some(lines) =
449 reports::get_all_lines_of_dialogue_for_character(&screenplay, character)
450 else {
451 continue;
452 };
453 let Some(scenes_with_char_speaking) =
454 reports::get_all_scenes_with_character_speaking(&screenplay, &character)
455 else {
456 continue;
457 };
458 let filter_benchmark_start = Instant::now();
459 let Some(filtered_scenes) = reports::filter_scenes_by_locations(
460 &screenplay,
461 scenes_with_char_speaking,
462 vec![location_id],
463 ) else {
464 println!("----- NO LINES -----");
465 continue;
466 };
467 for (scn, sceneobj) in &filtered_scenes {
468 let Some(page) = &screenplay.pages.get(sceneobj.start.page) else {
469 panic!("Could not find page.");
470 };
471 let Some(scene_line) = page.lines.get(sceneobj.start.line) else {
472 panic!("Could not find line in this page.");
473 };
474 }
475
476 if filtered_scenes.len() == screenplay.scenes.len() {
477 panic!("NO SCENES ACTUALLY FILTERED!");
478 }
479 let Some(mut filtered_lines) =
480 reports::filter_lines_by_multiple_scenes(&screenplay, &lines, filtered_scenes)
481 else {
482 continue; };
484
485 let filter_bench_end = filter_benchmark_start.elapsed();
486 println!(
487 "TIME TAKEN TO FILTER DIALOGUE FOR THIS LOCATION: {:?}",
488 filter_bench_end
489 );
490
491 let mut wordcount: usize = 0;
492 for (coord, f_line) in &filtered_lines {
493 let mut line_str = String::new();
494 wordcount += f_line.text_elements.len();
495 f_line
497 .text_elements
498 .iter()
499 .map(|te| te.text.clone())
500 .for_each(|ts| {
501 if !line_str.is_empty() {
502 line_str.push(' ');
503 }
504 line_str.push_str(&ts)
505 });
506 println!(
507 "----- {:<40} | PAGE: {:>4} | LINE: {:>4}",
508 line_str, &coord.page, &coord.line
509 );
510 }
511 print!(
512 "----- LINES OF DIALOGUE FOR CHARACTER: {:?} | ",
513 filtered_lines.len()
514 );
515 println!("WORDS FOR CHARACTER: {:}", wordcount);
516 }
517 }
518
519 let print_pages: bool = false;
520
521 for page in screenplay.pages {
527 if !print_pages {
528 break;
529 }
530 println!("PAGE: {:?}", page.page_number);
531 for line in page.lines {
532 println!(
533 "-----LINE | Y: {:?} | TYPE: {:?} | REVISED: {} | NUM: {:?}",
534 {
535 if let Some(te) = line.text_elements.first() {
536 if let Some(ep) = te.element_position {
537 Some(ep.y)
538 } else {
539 None
540 }
541 } else {
542 None
543 }
544 },
545 line.line_type,
546 line.revised,
547 line.scene_number
548 );
549 for elm in line.text_elements {
550 println!(
551 "{:38} | X: {:7.2?} '{}' ",
552 format!("{:?}", elm.element_type),
553 elm.element_position.unwrap().x,
554 elm.text,
555 );
556 }
557 println!("\n");
558 }
559 }
560 println!("\nTime elapsed: {:?}", start.elapsed());
561 }
562
563 #[test]
564 fn all_screenplay_element_types() {
565 let indentations = ElementIndentationsPoints::us_letter_default(&None);
573
574 println!(" ------ Testing Screenplay Element Types ------ ");
575 println!("");
576
577 let mut mock_pdf: pdf_document::PDFDocument = PDFDocument::default();
578 let mut new_page = pdf_document::Page::default();
579
580 new_page.lines.push(_create_pdfline_with_word(
581 "Action!".to_string(),
582 indentations.action,
583 None,
584 ));
585 new_page.lines.push(_create_pdfline_with_word(
586 "CHARACTER".to_string(),
587 indentations.character,
588 None,
589 ));
590 new_page.lines.push(_create_pdfline_with_word(
591 "(wryly)".to_string(),
592 indentations.parenthetical,
593 None,
594 ));
595 new_page.lines.push(_create_pdfline_with_word(
596 "Dialogue".to_string(),
597 indentations.dialogue,
598 None,
599 ));
600
601 let pn: String = "256ABC.".to_string();
602
603 let mut page_num_line = pdf_document::Line::default();
607 page_num_line.words.push(_create_pdfword(
608 "(26/04/25)".to_string(),
609 indentations.character,
610 Some(indentations.top),
611 ));
612 page_num_line.words.push(_create_pdfword(
613 pn.clone(),
614 (7.5 * 72.0) - (7.2 * pn.len() as f64),
615 Some(indentations.top),
616 ));
617
618 new_page.lines.push(page_num_line);
619
620 let mut revised_line = pdf_document::Line::default();
622
623 revised_line.words.push(_create_pdfword(
624 "revised_scn".to_string(),
625 indentations.action,
626 None,
627 ));
628 revised_line.words.push(_create_pdfword(
629 "*".to_string(),
630 (7.5 * 72.0) + (7.2 * 2.0),
631 None,
632 ));
633 new_page.lines.push(revised_line);
634
635 new_page.lines.push(_create_pdfline_with_word(
641 "(MORE)".to_string(),
642 indentations.parenthetical,
643 Some(60.0),
644 ));
645
646 new_page.lines.push(get_scene_heading_line(
648 "INT.",
649 "HOUSE - DAY - CONTINUOUS",
650 "*46G*",
651 &indentations,
652 ));
653
654 mock_pdf.pages.push(new_page);
674
675 let parsed_doc =
676 pdf_parser::get_screenplay_doc_from_pdf_obj(mock_pdf, None, None, None, None).unwrap();
677
678 println!(
679 "\n-----\n\nPage number: {:>8} | Rev. label/date(?): {:12} | {}\n",
680 format!("{:?}", parsed_doc.pages.first().unwrap().page_number),
681 format!("{:?}", parsed_doc.pages.first().unwrap().revision_label),
682 format!("{:?}", parsed_doc.pages.first().unwrap().revision_date),
683 );
684
685 let lines = &parsed_doc.pages.first().unwrap().lines;
686
687 for line in lines {
690 println!(
691 "LT: {:-<70} \nScene Num: {:8} \nRevised: {}",
692 if let Some(l_type) = &line.line_type {
693 format!("{:?}", l_type)
694 .strip_prefix("SP_")
695 .unwrap()
696 .to_string()
697 } else {
698 format!("{:?}", SPType::NONE)
699 },
700 if let Some(sc_num) = line.scene_number.clone() {
701 sc_num
702 } else {
703 "None".to_string()
704 },
705 if line.revised { "Y" } else { "N" },
706 );
707 println!("{:^30}|{:^8}{:^8}|{:^8}", "Element", "x", "y", "Text");
708 println!("{:-<58}", " -");
709 for el in &line.text_elements {
711 println!(
712 " {:24} | {:.2}, {:.2} | '{}'",
713 if let Some(l_type) = el.element_type.clone() {
714 format!("{:?}", l_type)
715 .strip_prefix("SP_")
716 .unwrap_or(&format!("{:?}", l_type))
717 .to_string()
718 } else {
719 format!("{:?}", SPType::NONE)
720 },
721 el.element_position.unwrap().x,
722 el.element_position.unwrap().y,
723 el.text,
724 );
725 }
726 println!("");
727 }
728
729 println!("{:#?}", parsed_doc.scenes)
732 }
733
734 fn get_scene_heading_line(
735 env: &str,
736 text: &str,
737 scn_num: &str,
738 indentations: &ElementIndentationsPoints,
739 ) -> pdf_document::Line {
740 let mut scene_heading_line = pdf_document::Line::default();
741
742 scene_heading_line
743 .words
744 .push(_create_pdfword(env.to_string(), indentations.action, None));
745 let mut last_word: String = env.to_string();
746 let mut last_word_pos: f64 = scene_heading_line.words.last().unwrap().position.x;
747 let mut _get_word_with_offset_from_previous = |text: String| {
748 let new_x_offset = (last_word.len() as f64 * 7.2) + 7.2 + last_word_pos;
750
751 let new_word = _create_pdfword(text.clone(), new_x_offset, None);
753
754 last_word = text.clone();
755 last_word_pos = new_x_offset;
756 return new_word;
757 };
758
759 let scene_heading_words = text.split_whitespace();
760
761 for word in scene_heading_words {
762 scene_heading_line
763 .words
764 .push(_get_word_with_offset_from_previous(word.to_string()));
765 }
766
767 scene_heading_line.words.push(_create_pdfword(
768 scn_num.to_string(),
769 indentations.right,
770 None,
771 ));
772
773 return scene_heading_line;
774 }
775
776 #[test]
779 fn scene_parsing() {
780 let indentations = ElementIndentationsPoints::us_letter_default(&None);
781
782 println!(" ------ Scene Parsing ------ ");
783 println!("");
784
785 let mut mock_pdf: pdf_document::PDFDocument = PDFDocument::default();
786 let mut new_page = pdf_document::Page::default();
787
788 let scene_heading_line =
789 get_scene_heading_line("INT.", "HOUSE - NIGHT - CONTINUOUS", "1A", &indentations);
790
791 new_page.lines.push(scene_heading_line);
792
793 mock_pdf.pages.push(new_page);
794
795 let mut second_page = pdf_document::Page::default();
796 let second_line = get_scene_heading_line(
797 "EXT.",
798 "BASEBALL FIELD - PITCHER'S MOUND - EARLIER - NIGHT",
799 "6G",
800 &indentations,
801 );
802
803 second_page.lines.push(second_line);
804 mock_pdf.pages.push(second_page);
805
806 let parsed_doc =
807 pdf_parser::get_screenplay_doc_from_pdf_obj(mock_pdf, None, None, None, None);
808
809 println!("{:#?}", parsed_doc);
810 }
811}