Skip to main content

screenplay_doc_parser_rs/
lib.rs

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            //"test_data/DraftTest_02.pdf".into(),
87            //"test_data/VCR2L.pdf".into(),
88            "/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/DraftTest_02.pdf".into(),
137            "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!("PAGESLEN: {:?}", &screenplay.pages.len());
149        //for page in &screenplay.pages {
150        //    println!("     PAGE: {:?} | LINES: {:?}", page.page_number, page.lines.len())
151        //}
152
153        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        // Test Get LOCATIONS...
188
189        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        // Test GET CHARACTER...
249
250        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        // Test ALL LINES OF DIALOGUE per Character
307        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                //panic!();
317                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                //println!("WORDS FOR LINE: {:}", line.text_elements.len());
330                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                //println!("{}",line_str);
340            }
341            println!("WORDS FOR CHARACTER: {:}", wordcount);
342        }
343
344        // Test Get SCENES per Character
345        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        // Test GET LOCATIONS
373
374        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        // Test GET PAGES
409        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                //panic!();
427                continue;
428            };
429            for (pidx, _page) in pages {
430                println!("----- {:?}", pidx)
431            }
432        }
433
434        // print filtered dialogue...
435
436        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; // all lines should categorically be part of SOME scene... unless there's ZERO "proper" scene headings...
483                };
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                    //println!("WORDS FOR LINE: {:}", line.text_elements.len());
496                    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        //println!("PAGESLEN: {:?}", screenplay.pages.len());
522        //for page in &screenplay.pages {
523        //    println!("     PAGE: {:?} | LINES: {:?}", page.page_number, page.lines.len())
524        //}
525
526        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        //TODO: Use a LineCounter variable to decrement the line height for
566        // each subsequent line
567        // double spacing for unrelated lines types,
568        // Single spacing for CHARACTER, PARENTHETICAL, and DIALOGUE since
569        // they are close together like that
570        // single spacing is 12.0 since the default font is 12-point courier
571
572        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        // Page Number / Page Revision Date
604        // Rests at y-height of top margin
605        // Is right-aligned to the right-hand margin
606        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        // Revised line
621        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        //TODO: CONTINUED/MOREs
636        // FIXME: How do we handle these?
637        // They are part of the document content.
638        // Also, we need to let the user pass in custom (MORE)/(CONTINUED) patterns
639        // again, for non-english or non-standard support.
640        new_page.lines.push(_create_pdfline_with_word(
641            "(MORE)".to_string(),
642            indentations.parenthetical,
643            Some(60.0),
644        ));
645
646        // TODO: Scene heading elements
647        new_page.lines.push(get_scene_heading_line(
648            "INT.",
649            "HOUSE - DAY - CONTINUOUS",
650            "*46G*",
651            &indentations,
652        ));
653
654        // TODO: Add DEFAULT INDENTATIONS for A4
655        // TODO: Test for A4 specifically
656
657        // maybe move the contents of this function to another function, which takes in the indentations
658        // as a parameter,
659        // then call the new function with arbitrary indentation values
660
661        // TODO: Revision LABEL (Blue:mm/dd/yyyy)
662
663        // TODO: Title Page elements
664
665        // TODO: Add TRANSITIONS??
666        // Check if first word is x-position past like 3/4ths or 2/3rds of the page
667        // if it's within the VCZ, and it's a farther-than-dialogue x-position,
668        //  AND it's the FIRST VALID TEXT ELEMENT, then it is likely a transition...
669        // Transitions like CUT TO or FADE OUT or FADE TO BLACK are not handled currently at all...
670        // Need to add to SP_TYPE enum...
671        // --------------
672
673        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        // TODO: panic!() for each line type that doesn't fully pass
688        // this means iterating manually... :<
689        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            //println!("---");
710            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        //TODO: Add SEPARATE test function just for Scene Parsing
730
731        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            //println!("last_word: {}, len_in_pts: {:?}", last_word, last_word.len() as f64 * 7.2);
749            let new_x_offset = (last_word.len() as f64 * 7.2) + 7.2 + last_word_pos;
750
751            //println!("offset x pos: {}", new_x_offset,);
752            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    // TODO: Implement and Test FORCING Scene Numbers for scenes that aren't assigned numbers
777    //
778    #[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}