From 3e745d427691a870443826e6c4b94dcdf00f7c9a Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 20 Feb 2026 15:03:23 +0100 Subject: [PATCH 01/39] Create greedy line break algorithm --- Cargo.lock | 1 + crates/rustwell/Cargo.toml | 1 + crates/rustwell/src/export.rs | 1 + crates/rustwell/src/export/pdf2.rs | 136 +++++++++++++++++++++++++++++ crates/rustwell/src/rich_string.rs | 25 ++++++ 5 files changed, 164 insertions(+) create mode 100644 crates/rustwell/src/export/pdf2.rs diff --git a/Cargo.lock b/Cargo.lock index 7260a4c..186569c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1880,6 +1880,7 @@ name = "rustwell" version = "0.1.0" dependencies = [ "bitflags 2.10.0", + "krilla", "typst", "typst-pdf", ] diff --git a/crates/rustwell/Cargo.toml b/crates/rustwell/Cargo.toml index b3d2cd5..26b4e59 100644 --- a/crates/rustwell/Cargo.toml +++ b/crates/rustwell/Cargo.toml @@ -13,5 +13,6 @@ path = "src/lib.rs" [dependencies] bitflags = "2" +krilla = "0.6.0" typst = "0.14.2" typst-pdf = "0.14.2" diff --git a/crates/rustwell/src/export.rs b/crates/rustwell/src/export.rs index a6caf68..1391993 100644 --- a/crates/rustwell/src/export.rs +++ b/crates/rustwell/src/export.rs @@ -10,6 +10,7 @@ pub mod html; pub mod pdf; +pub mod pdf2; pub mod typst; use std::{ diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs new file mode 100644 index 0000000..02d6d02 --- /dev/null +++ b/crates/rustwell/src/export/pdf2.rs @@ -0,0 +1,136 @@ +use crate::rich_string::RichString; + +const FONT_SIZE: usize = 12; +const FONT_WIDTH: f32 = 7.2; +const A4: (usize, usize) = (595, 842); + +fn symbol_span(point_span: usize, font_size: usize) -> usize { + point_span / font_size +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +enum BreakType { + NewLine, + BreakWord, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +struct BreakPoint { + pub index: usize, + pub break_type: BreakType, +} + +fn break_points(content: &RichString, span: usize) -> Vec { + if span < 2 { + panic!("Character span cannot be smaller than 2"); + } + + let mut brekpoints = Vec::with_capacity(content.len() / span + 1); + let mut last_space_char = (0, 0); + let mut line_len = 0; + for i in 0..content.len() { + let glyph = match content.get_char(i) { + Some(g) => g, + None => break, + }; + + line_len += 1; + if glyph == '\n' { + brekpoints.push(BreakPoint { + index: i, + break_type: BreakType::NewLine, + }); + line_len = 0; + continue; + } + + if glyph.is_whitespace() { + last_space_char = (brekpoints.len() + 1, i); + continue; + } + + if glyph == '-' { + last_space_char = (brekpoints.len() + 1, i); + continue; + } + + if line_len >= span { + if brekpoints.len() + 1 != last_space_char.0 { + brekpoints.push(BreakPoint { + index: i, + break_type: BreakType::BreakWord, + }); + line_len = 0; + continue; + } + + brekpoints.push(BreakPoint { + index: last_space_char.1 + 1, + break_type: BreakType::NewLine, + }); + line_len = i - last_space_char.1; + } + } + brekpoints +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn breaks_simple() { + let mut rs = RichString::new(); + rs.push_str("hello world"); + + let breakpoints = break_points(&rs, 6); + let correct = vec![BreakPoint { + index: 6, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_simple_with_newline() { + let mut rs = RichString::new(); + rs.push_str("hello\nworld"); + + let breakpoints = break_points(&rs, 100); + let correct = vec![BreakPoint { + index: 5, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_simple_breakword() { + let mut rs = RichString::new(); + rs.push_str("helloworld"); + + let breakpoints = break_points(&rs, 6); + let correct = vec![BreakPoint { + index: 5, + break_type: BreakType::BreakWord, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_simple_utilizing_hyphen() { + let mut rs = RichString::new(); + rs.push_str("hello-world"); + + let breakpoints = break_points(&rs, 7); + let correct = vec![BreakPoint { + index: 6, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } +} diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 0e3f4c5..679d713 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -14,6 +14,8 @@ //! assert!(rs.elements[1].is_bold()); //! ``` +use std::slice::SliceIndex; + use bitflags::bitflags; /// A string that can have different parts styled. @@ -55,6 +57,29 @@ impl RichString { } } + /// The total length of a [RichString], meaning the total number of [char]s. + pub fn len(&self) -> usize { + let mut len = 0; + for e in &self.elements { + len += e.text.len(); + } + len + } + + pub fn get_char(&self, mut index: usize) -> Option { + if index >= self.len() { + return None; + } + for e in &self.elements { + if index >= e.text.len() { + index -= e.text.len(); + continue; + } + return e.text.chars().nth(index); + } + None + } + /// Pushes a string onto the [RichString]. Will divide the string into multiple elements with /// different styles if input string can be parsed with styles. pub fn push_str(&mut self, str: impl AsRef) { From 1510f359d0675fe8467af1f3e22dd7a9a4ddd03a Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 20 Feb 2026 16:47:15 +0100 Subject: [PATCH 02/39] Implement (probably faulty) line writer --- crates/rustwell/src/export/pdf2.rs | 132 ++++++++++++++++++++++++++++- crates/rustwell/src/rich_string.rs | 14 +++ 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 02d6d02..5a32569 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -1,10 +1,134 @@ -use crate::rich_string::RichString; +use std::{io::Write, sync::Arc}; + +use krilla::{ + Document, + geom::Point, + surface::Surface, + text::{Font, TextDirection}, +}; + +use crate::{Exporter, Screenplay, rich_string::RichString}; const FONT_SIZE: usize = 12; -const FONT_WIDTH: f32 = 7.2; -const A4: (usize, usize) = (595, 842); +const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) + +/// The font bundled together with Rustwell; Courier Prime. +/// Includes the data of the font styles Regular, Bold, Italic +/// and BoldItalic, in stated order. +const FONTS: [&[u8]; 4] = [ + include_bytes!("fonts/CourierPrime-Regular.ttf"), + include_bytes!("fonts/CourierPrime-Bold.ttf"), + include_bytes!("fonts/CourierPrime-Italic.ttf"), + include_bytes!("fonts/CourierPrime-BoldItalic.ttf"), +]; + +struct Fonts { + pub regular: Font, + pub bold: Font, + pub italic: Font, + pub bold_italic: Font, +} + +struct PaperSize { + pub x: usize, + pub y: usize, +} + +const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts + +/// A [`Screenplay`] exporter for `pdf` +/// +/// The variables configure the exporter +#[derive(Default)] +pub struct Pdf2Exporter { + /// Whether to include synopses in the output + pub synopses: bool, +} + +impl Exporter for Pdf2Exporter { + fn file_extension(&self) -> &'static str { + "pdf" + } + + /// Exports a `pdf` file and writes it to the provided writer. + fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> { + let regular_data: Arc + Send + Sync> = Arc::new(FONTS[0]); + let bold_data: Arc + Send + Sync> = Arc::new(FONTS[1]); + let italic_data: Arc + Send + Sync> = Arc::new(FONTS[2]); + let bold_italic_data: Arc + Send + Sync> = Arc::new(FONTS[3]); + let mut document = Document::new(); + + let fonts = Fonts { + regular: Font::new(regular_data.into(), 0).unwrap(), + bold: Font::new(bold_data.into(), 0).unwrap(), + italic: Font::new(italic_data.into(), 0).unwrap(), + bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), + }; + + let pdf = document + .finish() + .map_err(|_| std::io::Error::other("failed to create pdf"))?; + writer.write_all(&pdf) + } +} + +fn write_line( + surface: &mut Surface, + x: f32, + y: f32, + content: &RichString, + mut start_index: usize, + breakpoint: &BreakPoint, + fonts: &Fonts, +) { + match content.get_char(start_index) { + Some(c) => { + if c == '\n' { + start_index += 1 + } + } + None => todo!(), + } + + let mut index = start_index; + let mut line_index = 0; + while index < breakpoint.index { + let (string_element, relative_index) = match content.get_element_from_index(index) { + Some(res) => res, + None => todo!(), + }; + + let relative_break_index = if breakpoint.index - index >= string_element.text.len() - index + { + string_element.text.len() + } else { + breakpoint.index - (index - relative_index) + }; + let font = match ( + string_element.is_bold(), + string_element.is_italic(), + string_element.is_underline(), + ) { + (false, false, _) => &fonts.regular, + (true, false, _) => &fonts.bold, + (false, true, _) => &fonts.italic, + (true, true, _) => &fonts.bold_italic, + }; + surface.draw_text( + Point::from_xy(x + (line_index as f32 * FONT_WIDTH), y), + font.clone(), + FONT_SIZE as f32, + &string_element.text[relative_index..relative_break_index], + false, + TextDirection::Auto, + ); + + line_index += relative_break_index - relative_index; + index = start_index + line_index; + } +} -fn symbol_span(point_span: usize, font_size: usize) -> usize { +fn glyph_span(point_span: usize, font_size: usize) -> usize { point_span / font_size } diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 679d713..511acb5 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -80,6 +80,20 @@ impl RichString { None } + pub fn get_element_from_index(&self, mut index: usize) -> Option<(&Element, usize)> { + if index >= self.len() { + return None; + } + for e in &self.elements { + if index >= e.text.len() { + index -= e.text.len(); + continue; + } + return Some((e, index)); + } + None + } + /// Pushes a string onto the [RichString]. Will divide the string into multiple elements with /// different styles if input string can be parsed with styles. pub fn push_str(&mut self, str: impl AsRef) { From 6a62de4d50257f1a61a715fc7dbe5b14547cd47c Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 20 Feb 2026 17:14:26 +0100 Subject: [PATCH 03/39] Finish the days work --- crates/rustwell/src/export/pdf2.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 5a32569..3c4774f 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -3,6 +3,7 @@ use std::{io::Write, sync::Arc}; use krilla::{ Document, geom::Point, + page::PageSettings, surface::Surface, text::{Font, TextDirection}, }; @@ -65,6 +66,8 @@ impl Exporter for Pdf2Exporter { bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), }; + generate_pdf(&mut document, &A4, screenplay, &fonts); + let pdf = document .finish() .map_err(|_| std::io::Error::other("failed to create pdf"))?; @@ -72,6 +75,19 @@ impl Exporter for Pdf2Exporter { } } +fn generate_pdf(document: &mut Document, size: &PaperSize, screenplay: &Screenplay, fonts: &Fonts) { + let mut screenplay_index = 0; + while screenplay_index < screenplay.elements.len() { + let mut page = + document.start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); + let mut surface = page.surface(); + let mut y: f64 = 0.0; + + surface.finish(); + page.finish(); + } +} + fn write_line( surface: &mut Surface, x: f32, From 738898624ab25d3ac4c175239b7169af2e19765d Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 20 Feb 2026 22:36:15 +0100 Subject: [PATCH 04/39] Surpise! The day isn't over --- crates/rustwell-cli/src/main.rs | 4 +- crates/rustwell/src/export/pdf2.rs | 179 ++++++++++++++++++++++++----- crates/rustwell/src/lib.rs | 1 + crates/rustwell/src/rich_string.rs | 12 +- 4 files changed, 156 insertions(+), 40 deletions(-) diff --git a/crates/rustwell-cli/src/main.rs b/crates/rustwell-cli/src/main.rs index 4c851c0..e4b3c5d 100644 --- a/crates/rustwell-cli/src/main.rs +++ b/crates/rustwell-cli/src/main.rs @@ -4,7 +4,7 @@ use color_eyre::eyre::bail; use rustwell::Exporter; use rustwell::ExporterExt; use rustwell::HtmlExporter; -use rustwell::PdfExporter; +use rustwell::Pdf2Exporter; use rustwell::Screenplay; use rustwell::TypstExporter; @@ -74,7 +74,7 @@ fn decide_exporter(cli: &Cli) -> Box { synopses: cli.synopses, ..Default::default() }), - Target::Pdf => Box::new(PdfExporter { + Target::Pdf => Box::new(Pdf2Exporter { synopses: cli.synopses, ..Default::default() }), diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 3c4774f..c6338d5 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -8,7 +8,7 @@ use krilla::{ text::{Font, TextDirection}, }; -use crate::{Exporter, Screenplay, rich_string::RichString}; +use crate::{Exporter, Screenplay, rich_string::RichString, screenplay::Element}; const FONT_SIZE: usize = 12; const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) @@ -36,6 +36,8 @@ struct PaperSize { } const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts +const TOP_MARGIN: usize = 72; +const BOTTOM_MARGIN: usize = 72; /// A [`Screenplay`] exporter for `pdf` /// @@ -77,24 +79,102 @@ impl Exporter for Pdf2Exporter { fn generate_pdf(document: &mut Document, size: &PaperSize, screenplay: &Screenplay, fonts: &Fonts) { let mut screenplay_index = 0; + + let max_lines = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; + let mut residual_index = None; + while screenplay_index < screenplay.elements.len() { let mut page = document.start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); let mut surface = page.surface(); - let mut y: f64 = 0.0; + let mut line_index = 0; + + loop { + if line_index >= max_lines { + break; + } + + if screenplay_index >= screenplay.elements.len() { + break; + } + + let element = &screenplay.elements[screenplay_index]; + let mut breakpoint_index = match residual_index { + Some(i) => { + residual_index = None; + i + } + None => 0, + }; + + match &element { + Element::Action(s) => write_element( + size, + s, + 108.0, + 72.0, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + ), + _ => unimplemented!(), + } + + line_index += 1; + screenplay_index += 1; + } surface.finish(); page.finish(); } } +fn write_element( + size: &PaperSize, + content: &RichString, + left_margin: f32, + right_margin: f32, + breakpoint_index: &mut usize, + line_index: &mut usize, + max_lines: usize, + surface: &mut Surface, + fonts: &Fonts, +) { + let span = glyph_span(size, left_margin, right_margin); + let breakpoints = break_points(content, span); + while *breakpoint_index <= breakpoints.len() { + if *line_index >= max_lines { + break; + } + + let start_index = if *breakpoint_index == 0 { + 0 + } else { + breakpoints[*breakpoint_index - 1].index + }; + write_line( + surface, + 108.0, + (FONT_SIZE * *line_index + TOP_MARGIN) as f32, + content, + start_index, + breakpoints.get(*breakpoint_index), + fonts, + ); + *breakpoint_index += 1; + *line_index += 1; + } +} + fn write_line( surface: &mut Surface, x: f32, y: f32, content: &RichString, mut start_index: usize, - breakpoint: &BreakPoint, + breakpoint: Option<&BreakPoint>, fonts: &Fonts, ) { match content.get_char(start_index) { @@ -106,19 +186,24 @@ fn write_line( None => todo!(), } - let mut index = start_index; - let mut line_index = 0; - while index < breakpoint.index { - let (string_element, relative_index) = match content.get_element_from_index(index) { + let breakpoint_index = match breakpoint { + Some(b) => b.index, + None => content.len(), + }; + + let mut glyph_index = 0; + while start_index < breakpoint_index { + let (string_element, relative_index) = match content.get_element_from_index(start_index) { Some(res) => res, None => todo!(), }; - let relative_break_index = if breakpoint.index - index >= string_element.text.len() - index + let relative_break_index = if breakpoint_index - start_index + >= string_element.text.chars().count() - relative_index { - string_element.text.len() + string_element.text.chars().count() } else { - breakpoint.index - (index - relative_index) + breakpoint_index - (start_index - relative_index) }; let font = match ( string_element.is_bold(), @@ -130,22 +215,33 @@ fn write_line( (false, true, _) => &fonts.italic, (true, true, _) => &fonts.bold_italic, }; + let mut char_indices = string_element.text.char_indices(); + let start_byte_index = char_indices.nth(relative_index).unwrap().0; + let end_byte_index = char_indices + .nth(relative_break_index - relative_index - 2) + .unwrap() + .0; + + dbg!(&string_element.text); + dbg!(&start_index); + dbg!(&breakpoint_index); + // dbg!(&end_byte_index); surface.draw_text( - Point::from_xy(x + (line_index as f32 * FONT_WIDTH), y), + Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), font.clone(), FONT_SIZE as f32, - &string_element.text[relative_index..relative_break_index], + &string_element.text[start_byte_index..=end_byte_index], false, TextDirection::Auto, ); - line_index += relative_break_index - relative_index; - index = start_index + line_index; + glyph_index += relative_break_index - relative_index; + start_index += relative_break_index - relative_index; } } -fn glyph_span(point_span: usize, font_size: usize) -> usize { - point_span / font_size +fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { + ((size.x as f32 - (left_margin + right_margin)) / FONT_WIDTH) as usize } #[derive(Debug, PartialEq, Eq, Clone, Hash)] @@ -161,17 +257,15 @@ struct BreakPoint { } fn break_points(content: &RichString, span: usize) -> Vec { - if span < 2 { - panic!("Character span cannot be smaller than 2"); - } + assert!(span >= 2); let mut brekpoints = Vec::with_capacity(content.len() / span + 1); - let mut last_space_char = (0, 0); + let mut last_whitespace_char = (0, 0); let mut line_len = 0; for i in 0..content.len() { let glyph = match content.get_char(i) { Some(g) => g, - None => break, + None => panic!(), }; line_len += 1; @@ -184,18 +278,13 @@ fn break_points(content: &RichString, span: usize) -> Vec { continue; } - if glyph.is_whitespace() { - last_space_char = (brekpoints.len() + 1, i); - continue; - } - - if glyph == '-' { - last_space_char = (brekpoints.len() + 1, i); + if glyph.is_whitespace() || glyph == '-' { + last_whitespace_char = (brekpoints.len() + 1, i); continue; } if line_len >= span { - if brekpoints.len() + 1 != last_space_char.0 { + if brekpoints.len() + 1 != last_whitespace_char.0 { brekpoints.push(BreakPoint { index: i, break_type: BreakType::BreakWord, @@ -205,10 +294,10 @@ fn break_points(content: &RichString, span: usize) -> Vec { } brekpoints.push(BreakPoint { - index: last_space_char.1 + 1, + index: last_whitespace_char.1 + 1, break_type: BreakType::NewLine, }); - line_len = i - last_space_char.1; + line_len = i - last_whitespace_char.1; } } brekpoints @@ -273,4 +362,32 @@ mod tests { assert_eq!(breakpoints, correct); } + + #[test] + fn breaks_rich() { + let mut rs = RichString::new(); + rs.push_str("he**ll**o wor*ld*"); + + let breakpoints = break_points(&rs, 6); + let correct = vec![BreakPoint { + index: 6, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_rich_longer() { + let mut rs = RichString::new(); + rs.push_str("Bosse går till **affären** och köper lite mjölk, vilket han tycker är väldigt gott att äta."); + + let breakpoints = break_points(&rs, 60); + let correct = vec![BreakPoint { + index: 56, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } } diff --git a/crates/rustwell/src/lib.rs b/crates/rustwell/src/lib.rs index 83eb74a..009bea9 100644 --- a/crates/rustwell/src/lib.rs +++ b/crates/rustwell/src/lib.rs @@ -43,6 +43,7 @@ pub use export::Exporter; pub use export::ExporterExt; pub use export::html::HtmlExporter; pub use export::pdf::PdfExporter; +pub use export::pdf2::Pdf2Exporter; pub use export::typst::TypstExporter; /// Parses a Fountain source string into a [Screenplay] structure. diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 511acb5..2993fbf 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -14,8 +14,6 @@ //! assert!(rs.elements[1].is_bold()); //! ``` -use std::slice::SliceIndex; - use bitflags::bitflags; /// A string that can have different parts styled. @@ -61,7 +59,7 @@ impl RichString { pub fn len(&self) -> usize { let mut len = 0; for e in &self.elements { - len += e.text.len(); + len += e.text.chars().count(); } len } @@ -71,8 +69,8 @@ impl RichString { return None; } for e in &self.elements { - if index >= e.text.len() { - index -= e.text.len(); + if index >= e.text.chars().count() { + index -= e.text.chars().count(); continue; } return e.text.chars().nth(index); @@ -85,8 +83,8 @@ impl RichString { return None; } for e in &self.elements { - if index >= e.text.len() { - index -= e.text.len(); + if index >= e.text.chars().count() { + index -= e.text.chars().count(); continue; } return Some((e, index)); From 43ac53040b208b65418a5438e37806d470cd189e Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 20 Feb 2026 22:49:11 +0100 Subject: [PATCH 05/39] Add closure --- crates/rustwell/src/export/pdf2.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index c6338d5..1ee18d0 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -107,19 +107,30 @@ fn generate_pdf(document: &mut Document, size: &PaperSize, screenplay: &Screenpl None => 0, }; - match &element { - Element::Action(s) => write_element( + let mut we = |content, left_magin, right_margin| { + write_element( size, - s, - 108.0, - 72.0, + content, + left_magin, + right_margin, &mut breakpoint_index, &mut line_index, max_lines, &mut surface, fonts, - ), - _ => unimplemented!(), + ) + }; + + match &element { + Element::Heading { slug, number } => we(slug, 108.0, 108.0), + Element::Action(s) => we(s, 108.0, 72.0), + Element::Dialogue(dialogue) => todo!(), + Element::DualDialogue(dialogue, dialogue1) => todo!(), + Element::Lyrics(rich_string) => todo!(), + Element::Transition(rich_string) => todo!(), + Element::CenteredText(rich_string) => todo!(), + Element::Synopsis(rich_string) => todo!(), + Element::PageBreak => break, } line_index += 1; @@ -222,10 +233,6 @@ fn write_line( .unwrap() .0; - dbg!(&string_element.text); - dbg!(&start_index); - dbg!(&breakpoint_index); - // dbg!(&end_byte_index); surface.draw_text( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), font.clone(), From bbd1c7755232037dd3ac4c4f145e57e6f2fc0228 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 21 Feb 2026 00:54:20 +0100 Subject: [PATCH 06/39] actually done for the day --- crates/rustwell/src/export/pdf2.rs | 194 ++++++++++++++++++----------- 1 file changed, 122 insertions(+), 72 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 1ee18d0..70fe4f2 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -1,11 +1,8 @@ use std::{io::Write, sync::Arc}; use krilla::{ - Document, - geom::Point, - page::PageSettings, - surface::Surface, - text::{Font, TextDirection}, + Document, color::rgb, geom::Point, num::NormalizedF32, page::PageSettings, paint::Fill, + surface::Surface, text::Font, }; use crate::{Exporter, Screenplay, rich_string::RichString, screenplay::Element}; @@ -68,7 +65,7 @@ impl Exporter for Pdf2Exporter { bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), }; - generate_pdf(&mut document, &A4, screenplay, &fonts); + self.generate_pdf(&mut document, &A4, screenplay, &fonts); let pdf = document .finish() @@ -77,68 +74,108 @@ impl Exporter for Pdf2Exporter { } } -fn generate_pdf(document: &mut Document, size: &PaperSize, screenplay: &Screenplay, fonts: &Fonts) { - let mut screenplay_index = 0; - - let max_lines = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; - let mut residual_index = None; - - while screenplay_index < screenplay.elements.len() { - let mut page = - document.start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); - let mut surface = page.surface(); - let mut line_index = 0; +// TODO: Remove when (if) krilla derives these traits. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum TextDirection { + LeftToRight, + RightToLeft, + Centered, +} - loop { - if line_index >= max_lines { - break; - } +impl Pdf2Exporter { + fn generate_pdf( + &self, + document: &mut Document, + size: &PaperSize, + screenplay: &Screenplay, + fonts: &Fonts, + ) { + let mut screenplay_index = 0; + + let max_lines = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; + let mut residual_index = None; + + while screenplay_index < screenplay.elements.len() { + let mut page = document + .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); + let mut surface = page.surface(); + let mut line_index = 0; + + loop { + if line_index >= max_lines { + break; + } - if screenplay_index >= screenplay.elements.len() { - break; - } + if screenplay_index >= screenplay.elements.len() { + break; + } - let element = &screenplay.elements[screenplay_index]; - let mut breakpoint_index = match residual_index { - Some(i) => { - residual_index = None; - i + let element = &screenplay.elements[screenplay_index]; + let mut breakpoint_index = match residual_index { + Some(i) => { + residual_index = None; + i + } + None => 0, + }; + + let mut we = |content, left_magin, right_margin, text_direction| { + write_element( + size, + content, + left_magin, + right_margin, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + text_direction, + ) + }; + + match &element { + Element::Heading { slug, number } => { + we(slug, 108.0, 72.0, TextDirection::LeftToRight) + } + Element::Action(s) => we(s, 108.0, 72.0, TextDirection::LeftToRight), + Element::Dialogue(dialogue) => todo!(), + Element::DualDialogue(dialogue, dialogue1) => todo!(), + Element::Lyrics(s) => todo!(), + Element::Transition(s) => we(s, 396.0, 72.0, TextDirection::RightToLeft), + Element::CenteredText(s) => we(s, 144.0, 144.0, TextDirection::Centered), + Element::Synopsis(s) => { + if self.synopses { + surface.set_fill(Some(Fill { + paint: rgb::Color::new(143, 143, 143).into(), + opacity: NormalizedF32::new(0.5).unwrap(), + rule: Default::default(), + })); + write_element( + size, + s, + 108.0, + 72.0, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + TextDirection::LeftToRight, + ); + surface.set_fill(None); + } + } + Element::PageBreak => break, } - None => 0, - }; - - let mut we = |content, left_magin, right_margin| { - write_element( - size, - content, - left_magin, - right_margin, - &mut breakpoint_index, - &mut line_index, - max_lines, - &mut surface, - fonts, - ) - }; - - match &element { - Element::Heading { slug, number } => we(slug, 108.0, 108.0), - Element::Action(s) => we(s, 108.0, 72.0), - Element::Dialogue(dialogue) => todo!(), - Element::DualDialogue(dialogue, dialogue1) => todo!(), - Element::Lyrics(rich_string) => todo!(), - Element::Transition(rich_string) => todo!(), - Element::CenteredText(rich_string) => todo!(), - Element::Synopsis(rich_string) => todo!(), - Element::PageBreak => break, + + line_index += 1; + screenplay_index += 1; } - line_index += 1; - screenplay_index += 1; + surface.finish(); + page.finish(); } - - surface.finish(); - page.finish(); } } @@ -152,6 +189,7 @@ fn write_element( max_lines: usize, surface: &mut Surface, fonts: &Fonts, + text_direction: TextDirection, ) { let span = glyph_span(size, left_margin, right_margin); let breakpoints = break_points(content, span); @@ -173,6 +211,8 @@ fn write_element( start_index, breakpoints.get(*breakpoint_index), fonts, + text_direction, + size, ); *breakpoint_index += 1; *line_index += 1; @@ -181,12 +221,14 @@ fn write_element( fn write_line( surface: &mut Surface, - x: f32, + mut x: f32, y: f32, content: &RichString, mut start_index: usize, breakpoint: Option<&BreakPoint>, fonts: &Fonts, + text_direction: TextDirection, + size: &PaperSize, ) { match content.get_char(start_index) { Some(c) => { @@ -202,6 +244,12 @@ fn write_line( None => content.len(), }; + if &text_direction == &TextDirection::Centered { + let line_length = breakpoint_index - start_index; + let line_span = (line_length / 2) as f32 * FONT_WIDTH; + x = (size.x / 2) as f32 - line_span; + } + let mut glyph_index = 0; while start_index < breakpoint_index { let (string_element, relative_index) = match content.get_element_from_index(start_index) { @@ -216,15 +264,11 @@ fn write_line( } else { breakpoint_index - (start_index - relative_index) }; - let font = match ( - string_element.is_bold(), - string_element.is_italic(), - string_element.is_underline(), - ) { - (false, false, _) => &fonts.regular, - (true, false, _) => &fonts.bold, - (false, true, _) => &fonts.italic, - (true, true, _) => &fonts.bold_italic, + let font = match (string_element.is_bold(), string_element.is_italic()) { + (false, false) => &fonts.regular, + (true, false) => &fonts.bold, + (false, true) => &fonts.italic, + (true, true) => &fonts.bold_italic, }; let mut char_indices = string_element.text.char_indices(); let start_byte_index = char_indices.nth(relative_index).unwrap().0; @@ -233,13 +277,19 @@ fn write_line( .unwrap() .0; + let td = match &text_direction { + &TextDirection::RightToLeft => krilla::text::TextDirection::RightToLeft, + &TextDirection::LeftToRight => krilla::text::TextDirection::LeftToRight, + &TextDirection::Centered => krilla::text::TextDirection::LeftToRight, + }; + surface.draw_text( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), font.clone(), FONT_SIZE as f32, &string_element.text[start_byte_index..=end_byte_index], false, - TextDirection::Auto, + td, ); glyph_index += relative_break_index - relative_index; From 9c55b395533bdf65025f20371a71c1ed128bd900 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 21 Feb 2026 13:07:35 +0100 Subject: [PATCH 07/39] Add dialogue (with some bugs) --- .gitignore | 3 + crates/rustwell/src/export/pdf2.rs | 108 +++++++++++++++++++++++------ 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index d20fd85..b6e7d2a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ target # Contains mutation testing data **/mutants.out*/ +# Input files for Rustwell +*.fountain + # Output files from Rustwell *.html *.pdf diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 70fe4f2..aa42c8e 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -5,7 +5,11 @@ use krilla::{ surface::Surface, text::Font, }; -use crate::{Exporter, Screenplay, rich_string::RichString, screenplay::Element}; +use crate::{ + Exporter, Screenplay, + rich_string::RichString, + screenplay::{DialogueElement, Element}, +}; const FONT_SIZE: usize = 12; const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) @@ -94,6 +98,7 @@ impl Pdf2Exporter { let max_lines = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; let mut residual_index = None; + let mut residual_dialogue = None; while screenplay_index < screenplay.elements.len() { let mut page = document @@ -101,7 +106,7 @@ impl Pdf2Exporter { let mut surface = page.surface(); let mut line_index = 0; - loop { + 'element_loop: loop { if line_index >= max_lines { break; } @@ -120,7 +125,7 @@ impl Pdf2Exporter { }; let mut we = |content, left_magin, right_margin, text_direction| { - write_element( + residual_index = write_element( size, content, left_magin, @@ -139,7 +144,65 @@ impl Pdf2Exporter { we(slug, 108.0, 72.0, TextDirection::LeftToRight) } Element::Action(s) => we(s, 108.0, 72.0, TextDirection::LeftToRight), - Element::Dialogue(dialogue) => todo!(), + Element::Dialogue(dialogue) => { + if line_index + 2 >= max_lines { + break; + } + residual_index = write_element( + size, + &dialogue.character, + 252.0, + 108.0, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + TextDirection::LeftToRight, + ); + let mut dialogue_index = residual_dialogue.unwrap_or(0); + while dialogue_index < dialogue.elements.len() { + if line_index >= max_lines { + residual_dialogue = Some(dialogue_index); + break 'element_loop; + } + + match &dialogue.elements[dialogue_index] { + DialogueElement::Parenthetical(s) => { + residual_index = write_element( + size, + &s, + 223.2, + 180.0, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + TextDirection::LeftToRight, + ) + } + DialogueElement::Line(s) => { + residual_index = write_element( + size, + &s, + 180.0, + 144.0, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + TextDirection::LeftToRight, + ) + } + } + + dialogue_index += 1; + } + + residual_dialogue = None; + } Element::DualDialogue(dialogue, dialogue1) => todo!(), Element::Lyrics(s) => todo!(), Element::Transition(s) => we(s, 396.0, 72.0, TextDirection::RightToLeft), @@ -166,7 +229,10 @@ impl Pdf2Exporter { surface.set_fill(None); } } - Element::PageBreak => break, + Element::PageBreak => { + screenplay_index += 1; + break; + } } line_index += 1; @@ -190,12 +256,12 @@ fn write_element( surface: &mut Surface, fonts: &Fonts, text_direction: TextDirection, -) { +) -> Option { let span = glyph_span(size, left_margin, right_margin); let breakpoints = break_points(content, span); while *breakpoint_index <= breakpoints.len() { if *line_index >= max_lines { - break; + return Some(*breakpoint_index); } let start_index = if *breakpoint_index == 0 { @@ -205,7 +271,7 @@ fn write_element( }; write_line( surface, - 108.0, + left_margin, (FONT_SIZE * *line_index + TOP_MARGIN) as f32, content, start_index, @@ -217,6 +283,7 @@ fn write_element( *breakpoint_index += 1; *line_index += 1; } + None } fn write_line( @@ -257,13 +324,14 @@ fn write_line( None => todo!(), }; - let relative_break_index = if breakpoint_index - start_index - >= string_element.text.chars().count() - relative_index - { - string_element.text.chars().count() - } else { - breakpoint_index - (start_index - relative_index) - }; + let element_length = string_element.text.chars().count(); + + let relative_break_index = + if breakpoint_index - start_index >= element_length - relative_index { + element_length + } else { + breakpoint_index - (start_index - relative_index) + }; let font = match (string_element.is_bold(), string_element.is_italic()) { (false, false) => &fonts.regular, (true, false) => &fonts.bold, @@ -272,10 +340,10 @@ fn write_line( }; let mut char_indices = string_element.text.char_indices(); let start_byte_index = char_indices.nth(relative_index).unwrap().0; - let end_byte_index = char_indices - .nth(relative_break_index - relative_index - 2) - .unwrap() - .0; + let end_byte_index = match char_indices.nth(relative_break_index - relative_index - 1) { + Some((i, _)) => i, + None => string_element.text.len(), + }; let td = match &text_direction { &TextDirection::RightToLeft => krilla::text::TextDirection::RightToLeft, @@ -287,7 +355,7 @@ fn write_line( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), font.clone(), FONT_SIZE as f32, - &string_element.text[start_byte_index..=end_byte_index], + &string_element.text[start_byte_index..end_byte_index], false, td, ); From 08fcddd969a063dc4e109f2c2d53b65cb223bc97 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 21 Feb 2026 19:38:24 +0100 Subject: [PATCH 08/39] Fix bug with dialogue --- crates/rustwell/src/export/pdf2.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index aa42c8e..79ada2a 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -148,6 +148,7 @@ impl Pdf2Exporter { if line_index + 2 >= max_lines { break; } + let old_breakpoint_index = breakpoint_index; residual_index = write_element( size, &dialogue.character, @@ -160,6 +161,8 @@ impl Pdf2Exporter { fonts, TextDirection::LeftToRight, ); + breakpoint_index = old_breakpoint_index; + let mut dialogue_index = residual_dialogue.unwrap_or(0); while dialogue_index < dialogue.elements.len() { if line_index >= max_lines { From 69148cfa7f52450f0e4822e01571bf335e54f209 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 25 Feb 2026 22:42:33 +0100 Subject: [PATCH 09/39] Attempt to introduce (more) and (cont'd) --- crates/rustwell/src/export/pdf2.rs | 141 ++++++++++++++++++++++------- crates/rustwell/src/rich_string.rs | 4 + 2 files changed, 114 insertions(+), 31 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 79ada2a..418c9ca 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -40,6 +40,62 @@ const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts const TOP_MARGIN: usize = 72; const BOTTOM_MARGIN: usize = 72; +struct Margin { + pub left: f32, + pub right: f32, +} + +struct Margins { + pub heading: Margin, + pub action: Margin, + pub character: Margin, + pub parenthetical: Margin, + pub dialogue: Margin, + pub lyrics: Margin, + pub transition: Margin, + pub centered: Margin, + pub synopsis: Margin, +} + +const MARGINS: Margins = Margins { + heading: Margin { + left: 108.0, + right: 72.0, + }, + action: Margin { + left: 108.0, + right: 72.0, + }, + character: Margin { + left: 252.0, + right: 108.0, + }, + parenthetical: Margin { + left: 223.2, + right: 180.0, + }, + dialogue: Margin { + left: 180.0, + right: 144.0, + }, + lyrics: Margin { + left: 180.0, + right: 144.0, + }, + transition: Margin { + left: 396.0, + right: 144.0, + }, + centered: Margin { + left: 144.0, + right: 144.0, + }, + synopsis: Margin { + left: 108.0, + right: 72.0, + }, +}; + /// A [`Screenplay`] exporter for `pdf` /// /// The variables configure the exporter @@ -80,7 +136,7 @@ impl Exporter for Pdf2Exporter { // TODO: Remove when (if) krilla derives these traits. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum TextDirection { +enum Alignment { LeftToRight, RightToLeft, Centered, @@ -124,12 +180,11 @@ impl Pdf2Exporter { None => 0, }; - let mut we = |content, left_magin, right_margin, text_direction| { + let mut we = |content, margin, text_direction| { residual_index = write_element( size, content, - left_magin, - right_margin, + margin, &mut breakpoint_index, &mut line_index, max_lines, @@ -141,25 +196,39 @@ impl Pdf2Exporter { match &element { Element::Heading { slug, number } => { - we(slug, 108.0, 72.0, TextDirection::LeftToRight) + we(slug, MARGINS.heading, Alignment::LeftToRight) } - Element::Action(s) => we(s, 108.0, 72.0, TextDirection::LeftToRight), + Element::Action(s) => we(s, MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { - if line_index + 2 >= max_lines { + let mut character_name = dialogue.character.clone(); + match (residual_dialogue, &dialogue.extension) { + (Some(_), _) => { + character_name.append(" (cont'd)".into()); + } + (None, Some(ext)) => { + character_name.append(" (".into()); + character_name.append(ext.clone()); + character_name.append(")".into()); + } + _ => (), + }; + let span = + glyph_span(size, MARGINS.character.left, MARGINS.character.right); + let name_lines_count = break_points(&character_name, span).len() + 1; + if line_index + name_lines_count + 1 >= max_lines { break; } let old_breakpoint_index = breakpoint_index; residual_index = write_element( size, - &dialogue.character, - 252.0, - 108.0, + &character_name, + MARGINS.character, &mut breakpoint_index, &mut line_index, max_lines, &mut surface, fonts, - TextDirection::LeftToRight, + Alignment::LeftToRight, ); breakpoint_index = old_breakpoint_index; @@ -167,6 +236,18 @@ impl Pdf2Exporter { while dialogue_index < dialogue.elements.len() { if line_index >= max_lines { residual_dialogue = Some(dialogue_index); + write_element( + size, + &"(MORE)".into(), + MARGINS.character, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + Alignment::LeftToRight, + ); + break 'element_loop; } @@ -175,28 +256,26 @@ impl Pdf2Exporter { residual_index = write_element( size, &s, - 223.2, - 180.0, + MARGINS.parenthetical, &mut breakpoint_index, &mut line_index, max_lines, &mut surface, fonts, - TextDirection::LeftToRight, + Alignment::LeftToRight, ) } DialogueElement::Line(s) => { residual_index = write_element( size, &s, - 180.0, - 144.0, + MARGINS.dialogue, &mut breakpoint_index, &mut line_index, max_lines, &mut surface, fonts, - TextDirection::LeftToRight, + Alignment::LeftToRight, ) } } @@ -208,8 +287,8 @@ impl Pdf2Exporter { } Element::DualDialogue(dialogue, dialogue1) => todo!(), Element::Lyrics(s) => todo!(), - Element::Transition(s) => we(s, 396.0, 72.0, TextDirection::RightToLeft), - Element::CenteredText(s) => we(s, 144.0, 144.0, TextDirection::Centered), + Element::Transition(s) => we(s, MARGINS.transition, Alignment::RightToLeft), + Element::CenteredText(s) => we(s, MARGINS.centered, Alignment::Centered), Element::Synopsis(s) => { if self.synopses { surface.set_fill(Some(Fill { @@ -220,14 +299,13 @@ impl Pdf2Exporter { write_element( size, s, - 108.0, - 72.0, + MARGINS.synopsis, &mut breakpoint_index, &mut line_index, max_lines, &mut surface, fonts, - TextDirection::LeftToRight, + Alignment::LeftToRight, ); surface.set_fill(None); } @@ -251,15 +329,16 @@ impl Pdf2Exporter { fn write_element( size: &PaperSize, content: &RichString, - left_margin: f32, - right_margin: f32, + margin: Margin, breakpoint_index: &mut usize, line_index: &mut usize, max_lines: usize, surface: &mut Surface, fonts: &Fonts, - text_direction: TextDirection, + text_direction: Alignment, ) -> Option { + let left_margin = margin.left; + let right_margin = margin.right; let span = glyph_span(size, left_margin, right_margin); let breakpoints = break_points(content, span); while *breakpoint_index <= breakpoints.len() { @@ -297,7 +376,7 @@ fn write_line( mut start_index: usize, breakpoint: Option<&BreakPoint>, fonts: &Fonts, - text_direction: TextDirection, + text_direction: Alignment, size: &PaperSize, ) { match content.get_char(start_index) { @@ -314,7 +393,7 @@ fn write_line( None => content.len(), }; - if &text_direction == &TextDirection::Centered { + if &text_direction == &Alignment::Centered { let line_length = breakpoint_index - start_index; let line_span = (line_length / 2) as f32 * FONT_WIDTH; x = (size.x / 2) as f32 - line_span; @@ -349,9 +428,9 @@ fn write_line( }; let td = match &text_direction { - &TextDirection::RightToLeft => krilla::text::TextDirection::RightToLeft, - &TextDirection::LeftToRight => krilla::text::TextDirection::LeftToRight, - &TextDirection::Centered => krilla::text::TextDirection::LeftToRight, + &Alignment::RightToLeft => krilla::text::TextDirection::RightToLeft, + &Alignment::LeftToRight => krilla::text::TextDirection::LeftToRight, + &Alignment::Centered => krilla::text::TextDirection::LeftToRight, }; surface.draw_text( @@ -385,7 +464,7 @@ struct BreakPoint { } fn break_points(content: &RichString, span: usize) -> Vec { - assert!(span >= 2); + debug_assert!(span >= 2); let mut brekpoints = Vec::with_capacity(content.len() / span + 1); let mut last_whitespace_char = (0, 0); diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 2993fbf..d0ac42b 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -92,6 +92,10 @@ impl RichString { None } + pub fn append(&mut self, mut other: Self) { + self.elements.append(&mut other.elements); + } + /// Pushes a string onto the [RichString]. Will divide the string into multiple elements with /// different styles if input string can be parsed with styles. pub fn push_str(&mut self, str: impl AsRef) { From 2850f1f8954f854fa6f2918e9265a111428ba5b3 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Thu, 26 Feb 2026 22:05:48 +0100 Subject: [PATCH 10/39] More and cont'd works --- crates/rustwell/src/export/pdf2.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 418c9ca..b5aa81a 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -218,31 +218,32 @@ impl Pdf2Exporter { if line_index + name_lines_count + 1 >= max_lines { break; } - let old_breakpoint_index = breakpoint_index; + if name_lines_count > max_lines { + panic!("Character name cannot be longer than page"); + } residual_index = write_element( size, &character_name, MARGINS.character, - &mut breakpoint_index, + &mut 0, &mut line_index, max_lines, &mut surface, fonts, Alignment::LeftToRight, ); - breakpoint_index = old_breakpoint_index; let mut dialogue_index = residual_dialogue.unwrap_or(0); while dialogue_index < dialogue.elements.len() { - if line_index >= max_lines { + if line_index + 1 >= max_lines { residual_dialogue = Some(dialogue_index); write_element( size, &"(MORE)".into(), MARGINS.character, - &mut breakpoint_index, + &mut 0, &mut line_index, - max_lines, + max_lines + 1, &mut surface, fonts, Alignment::LeftToRight, @@ -250,6 +251,13 @@ impl Pdf2Exporter { break 'element_loop; } + let mut breakpoint_index = match residual_index { + Some(i) => { + residual_index = None; + i + } + None => 0, + }; match &dialogue.elements[dialogue_index] { DialogueElement::Parenthetical(s) => { @@ -280,13 +288,17 @@ impl Pdf2Exporter { } } + if residual_index.is_some() { + continue; + } + dialogue_index += 1; } residual_dialogue = None; } Element::DualDialogue(dialogue, dialogue1) => todo!(), - Element::Lyrics(s) => todo!(), + Element::Lyrics(s) => we(s, MARGINS.lyrics, Alignment::RightToLeft), Element::Transition(s) => we(s, MARGINS.transition, Alignment::RightToLeft), Element::CenteredText(s) => we(s, MARGINS.centered, Alignment::Centered), Element::Synopsis(s) => { From b0407bdaacd9874d1b768080d2389144ca854a96 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 27 Feb 2026 19:08:50 +0100 Subject: [PATCH 11/39] Add rich string iterator --- crates/rustwell/src/export/pdf2.rs | 7 +------ crates/rustwell/src/rich_string.rs | 33 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index b5aa81a..f1da784 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -481,12 +481,7 @@ fn break_points(content: &RichString, span: usize) -> Vec { let mut brekpoints = Vec::with_capacity(content.len() / span + 1); let mut last_whitespace_char = (0, 0); let mut line_len = 0; - for i in 0..content.len() { - let glyph = match content.get_char(i) { - Some(g) => g, - None => panic!(), - }; - + for (i, glyph) in content.iter().enumerate() { line_len += 1; if glyph == '\n' { brekpoints.push(BreakPoint { diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index d0ac42b..db27bf1 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -14,6 +14,8 @@ //! assert!(rs.elements[1].is_bold()); //! ``` +use std::str::Chars; + use bitflags::bitflags; /// A string that can have different parts styled. @@ -92,6 +94,14 @@ impl RichString { None } + pub fn iter(&'_ self) -> RichIterator<'_> { + RichIterator { + rich_string: self, + element_idx: 0, + chars_iterator: self.elements[0].text.chars(), + } + } + pub fn append(&mut self, mut other: Self) { self.elements.append(&mut other.elements); } @@ -195,6 +205,29 @@ where } } +pub struct RichIterator<'a> { + rich_string: &'a RichString, + element_idx: usize, + chars_iterator: Chars<'a>, +} + +impl<'a> Iterator for RichIterator<'a> { + type Item = char; + + fn next(&mut self) -> Option { + let next = self.chars_iterator.next(); + if next.is_some() { + return next; + } + self.element_idx += 1; + if self.element_idx >= self.rich_string.elements.len() { + return None; + } + self.chars_iterator = self.rich_string.elements[self.element_idx].text.chars(); + self.chars_iterator.next() + } +} + /// A [RichString] component, containing a [String] and the style attributes /// belonging to said string. #[derive(Debug, PartialEq, Eq, Clone, Default, Hash)] From 1f6e35965db41c1df355a78751752e70e36d5ac2 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 27 Feb 2026 19:40:13 +0100 Subject: [PATCH 12/39] Introduce letter paper :( --- crates/rustwell/src/export/pdf2.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index f1da784..c14a032 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -31,12 +31,14 @@ struct Fonts { pub bold_italic: Font, } -struct PaperSize { +pub struct PaperSize { pub x: usize, pub y: usize, } -const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts +pub const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts +pub const LETTER: PaperSize = PaperSize { x: 612, y: 792 }; // Letter size in pts + const TOP_MARGIN: usize = 72; const BOTTOM_MARGIN: usize = 72; @@ -134,7 +136,6 @@ impl Exporter for Pdf2Exporter { } } -// TODO: Remove when (if) krilla derives these traits. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum Alignment { LeftToRight, From e1742c379354a315040e46ddd56bd686e835f0b6 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 27 Feb 2026 22:00:34 +0100 Subject: [PATCH 13/39] refactor dialogue and start on dual dialogue --- crates/rustwell/src/export/pdf2.rs | 232 ++++++++++++++++++----------- 1 file changed, 142 insertions(+), 90 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index c14a032..455f2c7 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -8,7 +8,7 @@ use krilla::{ use crate::{ Exporter, Screenplay, rich_string::RichString, - screenplay::{DialogueElement, Element}, + screenplay::{Dialogue, DialogueElement, Element}, }; const FONT_SIZE: usize = 12; @@ -157,13 +157,15 @@ impl Pdf2Exporter { let mut residual_index = None; let mut residual_dialogue = None; + let mut residual_dual_dialogue = (None, None); + while screenplay_index < screenplay.elements.len() { let mut page = document .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); let mut surface = page.surface(); let mut line_index = 0; - 'element_loop: loop { + loop { if line_index >= max_lines { break; } @@ -201,104 +203,48 @@ impl Pdf2Exporter { } Element::Action(s) => we(s, MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { - let mut character_name = dialogue.character.clone(); - match (residual_dialogue, &dialogue.extension) { - (Some(_), _) => { - character_name.append(" (cont'd)".into()); - } - (None, Some(ext)) => { - character_name.append(" (".into()); - character_name.append(ext.clone()); - character_name.append(")".into()); - } - _ => (), - }; - let span = - glyph_span(size, MARGINS.character.left, MARGINS.character.right); - let name_lines_count = break_points(&character_name, span).len() + 1; - if line_index + name_lines_count + 1 >= max_lines { + write_dialogue( + dialogue, + &mut residual_dialogue, + &mut residual_index, + size, + max_lines, + &mut line_index, + &mut surface, + fonts, + ); + if residual_dialogue.is_some() { break; } - if name_lines_count > max_lines { - panic!("Character name cannot be longer than page"); - } - residual_index = write_element( + } + Element::DualDialogue(dialogue0, dialogue1) => { + let mut initial_line_index = line_index; + write_dialogue( + dialogue0, + &mut residual_dual_dialogue.0, + &mut residual_index, size, - &character_name, - MARGINS.character, - &mut 0, + max_lines, &mut line_index, + &mut surface, + fonts, + ); + write_dialogue( + dialogue1, + &mut residual_dual_dialogue.1, + &mut residual_index, + size, max_lines, + &mut initial_line_index, &mut surface, fonts, - Alignment::LeftToRight, ); - - let mut dialogue_index = residual_dialogue.unwrap_or(0); - while dialogue_index < dialogue.elements.len() { - if line_index + 1 >= max_lines { - residual_dialogue = Some(dialogue_index); - write_element( - size, - &"(MORE)".into(), - MARGINS.character, - &mut 0, - &mut line_index, - max_lines + 1, - &mut surface, - fonts, - Alignment::LeftToRight, - ); - - break 'element_loop; - } - let mut breakpoint_index = match residual_index { - Some(i) => { - residual_index = None; - i - } - None => 0, - }; - - match &dialogue.elements[dialogue_index] { - DialogueElement::Parenthetical(s) => { - residual_index = write_element( - size, - &s, - MARGINS.parenthetical, - &mut breakpoint_index, - &mut line_index, - max_lines, - &mut surface, - fonts, - Alignment::LeftToRight, - ) - } - DialogueElement::Line(s) => { - residual_index = write_element( - size, - &s, - MARGINS.dialogue, - &mut breakpoint_index, - &mut line_index, - max_lines, - &mut surface, - fonts, - Alignment::LeftToRight, - ) - } - } - - if residual_index.is_some() { - continue; - } - - dialogue_index += 1; + line_index = line_index.max(initial_line_index); + if residual_dual_dialogue.0.is_some() || residual_dual_dialogue.1.is_some() + { + break; } - - residual_dialogue = None; } - Element::DualDialogue(dialogue, dialogue1) => todo!(), Element::Lyrics(s) => we(s, MARGINS.lyrics, Alignment::RightToLeft), Element::Transition(s) => we(s, MARGINS.transition, Alignment::RightToLeft), Element::CenteredText(s) => we(s, MARGINS.centered, Alignment::Centered), @@ -339,6 +285,112 @@ impl Pdf2Exporter { } } +fn write_dialogue( + dialogue: &Dialogue, + residual_dialogue: &mut Option, + residual_index: &mut Option, + size: &PaperSize, + max_lines: usize, + line_index: &mut usize, + surface: &mut Surface, + fonts: &Fonts, +) { + let mut character_name = dialogue.character.clone(); + match (*residual_dialogue, &dialogue.extension) { + (Some(_), _) => { + character_name.append(" (cont'd)".into()); + } + (None, Some(ext)) => { + character_name.append(" (".into()); + character_name.append(ext.clone()); + character_name.append(")".into()); + } + _ => (), + }; + let span = glyph_span(size, MARGINS.character.left, MARGINS.character.right); + let name_lines_count = break_points(&character_name, span).len() + 1; + if *line_index + name_lines_count + 1 >= max_lines { + return; + } + assert!(name_lines_count < max_lines); + + *residual_index = write_element( + size, + &character_name, + MARGINS.character, + &mut 0, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + ); + + let mut dialogue_index = residual_dialogue.unwrap_or(0); + while dialogue_index < dialogue.elements.len() { + if *line_index + 1 >= max_lines { + *residual_dialogue = Some(dialogue_index); + write_element( + size, + &"(MORE)".into(), + MARGINS.character, + &mut 0, + line_index, + max_lines + 1, + surface, + fonts, + Alignment::LeftToRight, + ); + + return; + } + let mut breakpoint_index = match *residual_index { + Some(i) => { + *residual_index = None; + i + } + None => 0, + }; + + match &dialogue.elements[dialogue_index] { + DialogueElement::Parenthetical(s) => { + *residual_index = write_element( + size, + &s, + MARGINS.parenthetical, + &mut breakpoint_index, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + ) + } + DialogueElement::Line(s) => { + *residual_index = write_element( + size, + &s, + MARGINS.dialogue, + &mut breakpoint_index, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + ) + } + } + + if residual_index.is_some() { + continue; + } + + dialogue_index += 1; + } + + *residual_dialogue = None; +} + fn write_element( size: &PaperSize, content: &RichString, From 1967c9103ba18942519499e265f9a4c41f1d4b9b Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 28 Feb 2026 12:34:46 +0100 Subject: [PATCH 14/39] Implement dual dialogue --- crates/rustwell/src/export/pdf2.rs | 154 ++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 46 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 455f2c7..00dc332 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -42,17 +42,28 @@ pub const LETTER: PaperSize = PaperSize { x: 612, y: 792 }; // Letter size in pt const TOP_MARGIN: usize = 72; const BOTTOM_MARGIN: usize = 72; +// #[derive(Clone, Copy)] struct Margin { pub left: f32, pub right: f32, } +struct DialogueMargins { + pub character: Margin, + pub parenthetical: Margin, + pub line: Margin, +} + +struct DualDialogueMargins { + pub left: DialogueMargins, + pub right: DialogueMargins, +} + struct Margins { pub heading: Margin, pub action: Margin, - pub character: Margin, - pub parenthetical: Margin, - pub dialogue: Margin, + pub dialogue: DialogueMargins, + pub dual_dialogue: DualDialogueMargins, pub lyrics: Margin, pub transition: Margin, pub centered: Margin, @@ -68,17 +79,49 @@ const MARGINS: Margins = Margins { left: 108.0, right: 72.0, }, - character: Margin { - left: 252.0, - right: 108.0, - }, - parenthetical: Margin { - left: 223.2, - right: 180.0, + dialogue: DialogueMargins { + character: Margin { + left: 252.0, + right: 108.0, + }, + parenthetical: Margin { + left: 223.2, + right: 180.0, + }, + line: Margin { + left: 180.0, + right: 144.0, + }, }, - dialogue: Margin { - left: 180.0, - right: 144.0, + dual_dialogue: DualDialogueMargins { + left: DialogueMargins { + character: Margin { + left: 198.0, + right: 288.0, + }, + parenthetical: Margin { + left: 162.0, + right: 324.0, + }, + line: Margin { + left: 144.0, + right: 288.0, + }, + }, + right: DialogueMargins { + character: Margin { + left: 414.0, + right: 72.0, + }, + parenthetical: Margin { + left: 378.0, + right: 90.0, + }, + line: Margin { + left: 360.0, + right: 72.0, + }, + }, }, lyrics: Margin { left: 180.0, @@ -158,6 +201,7 @@ impl Pdf2Exporter { let mut residual_dialogue = None; let mut residual_dual_dialogue = (None, None); + let mut residual_dual_index = (None, None); while screenplay_index < screenplay.elements.len() { let mut page = document @@ -199,9 +243,9 @@ impl Pdf2Exporter { match &element { Element::Heading { slug, number } => { - we(slug, MARGINS.heading, Alignment::LeftToRight) + we(slug, &MARGINS.heading, Alignment::LeftToRight) } - Element::Action(s) => we(s, MARGINS.action, Alignment::LeftToRight), + Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { write_dialogue( dialogue, @@ -212,6 +256,7 @@ impl Pdf2Exporter { &mut line_index, &mut surface, fonts, + &MARGINS.dialogue, ); if residual_dialogue.is_some() { break; @@ -219,35 +264,47 @@ impl Pdf2Exporter { } Element::DualDialogue(dialogue0, dialogue1) => { let mut initial_line_index = line_index; - write_dialogue( - dialogue0, - &mut residual_dual_dialogue.0, - &mut residual_index, - size, - max_lines, - &mut line_index, - &mut surface, - fonts, - ); - write_dialogue( - dialogue1, - &mut residual_dual_dialogue.1, - &mut residual_index, - size, - max_lines, - &mut initial_line_index, - &mut surface, - fonts, - ); + if (residual_dual_dialogue.0.is_none() + && residual_dual_dialogue.1.is_none()) + || residual_dual_dialogue.0.is_some() + { + write_dialogue( + dialogue0, + &mut residual_dual_dialogue.0, + &mut residual_dual_index.0, + size, + max_lines, + &mut line_index, + &mut surface, + fonts, + &MARGINS.dual_dialogue.left, + ); + } + if (residual_dual_dialogue.1.is_none() + && residual_dual_dialogue.0.is_none()) + || residual_dual_dialogue.1.is_some() + { + write_dialogue( + dialogue1, + &mut residual_dual_dialogue.1, + &mut residual_dual_index.1, + size, + max_lines, + &mut initial_line_index, + &mut surface, + fonts, + &MARGINS.dual_dialogue.right, + ); + } line_index = line_index.max(initial_line_index); if residual_dual_dialogue.0.is_some() || residual_dual_dialogue.1.is_some() { break; } } - Element::Lyrics(s) => we(s, MARGINS.lyrics, Alignment::RightToLeft), - Element::Transition(s) => we(s, MARGINS.transition, Alignment::RightToLeft), - Element::CenteredText(s) => we(s, MARGINS.centered, Alignment::Centered), + Element::Lyrics(s) => we(s, &MARGINS.lyrics, Alignment::RightToLeft), + Element::Transition(s) => we(s, &MARGINS.transition, Alignment::RightToLeft), + Element::CenteredText(s) => we(s, &MARGINS.centered, Alignment::Centered), Element::Synopsis(s) => { if self.synopses { surface.set_fill(Some(Fill { @@ -258,7 +315,7 @@ impl Pdf2Exporter { write_element( size, s, - MARGINS.synopsis, + &MARGINS.synopsis, &mut breakpoint_index, &mut line_index, max_lines, @@ -294,6 +351,7 @@ fn write_dialogue( line_index: &mut usize, surface: &mut Surface, fonts: &Fonts, + dialogue_margins: &DialogueMargins, ) { let mut character_name = dialogue.character.clone(); match (*residual_dialogue, &dialogue.extension) { @@ -307,17 +365,21 @@ fn write_dialogue( } _ => (), }; - let span = glyph_span(size, MARGINS.character.left, MARGINS.character.right); + let span = glyph_span( + size, + dialogue_margins.character.left, + dialogue_margins.character.right, + ); let name_lines_count = break_points(&character_name, span).len() + 1; if *line_index + name_lines_count + 1 >= max_lines { return; } assert!(name_lines_count < max_lines); - *residual_index = write_element( + write_element( size, &character_name, - MARGINS.character, + &dialogue_margins.character, &mut 0, line_index, max_lines, @@ -333,7 +395,7 @@ fn write_dialogue( write_element( size, &"(MORE)".into(), - MARGINS.character, + &dialogue_margins.character, &mut 0, line_index, max_lines + 1, @@ -357,7 +419,7 @@ fn write_dialogue( *residual_index = write_element( size, &s, - MARGINS.parenthetical, + &dialogue_margins.parenthetical, &mut breakpoint_index, line_index, max_lines, @@ -370,7 +432,7 @@ fn write_dialogue( *residual_index = write_element( size, &s, - MARGINS.dialogue, + &dialogue_margins.line, &mut breakpoint_index, line_index, max_lines, @@ -394,7 +456,7 @@ fn write_dialogue( fn write_element( size: &PaperSize, content: &RichString, - margin: Margin, + margin: &Margin, breakpoint_index: &mut usize, line_index: &mut usize, max_lines: usize, From 4efe30cdb72d633042d2f6ecc5de38cad966aa61 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 28 Feb 2026 13:45:23 +0100 Subject: [PATCH 15/39] Implement outline and start on (unfinished) scene numbers --- crates/rustwell/src/export/pdf2.rs | 48 +++++++++++++++++++++++++++--- crates/rustwell/src/rich_string.rs | 9 ++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 00dc332..fb440c1 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -1,8 +1,16 @@ use std::{io::Write, sync::Arc}; use krilla::{ - Document, color::rgb, geom::Point, num::NormalizedF32, page::PageSettings, paint::Fill, - surface::Surface, text::Font, + Document, + color::rgb, + destination::XyzDestination, + geom::Point, + num::NormalizedF32, + outline::{Outline, OutlineNode}, + page::PageSettings, + paint::Fill, + surface::Surface, + text::Font, }; use crate::{ @@ -42,7 +50,6 @@ pub const LETTER: PaperSize = PaperSize { x: 612, y: 792 }; // Letter size in pt const TOP_MARGIN: usize = 72; const BOTTOM_MARGIN: usize = 72; -// #[derive(Clone, Copy)] struct Margin { pub left: f32, pub right: f32, @@ -196,6 +203,8 @@ impl Pdf2Exporter { ) { let mut screenplay_index = 0; + let mut page_index = 0; + let max_lines = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; let mut residual_index = None; let mut residual_dialogue = None; @@ -203,6 +212,8 @@ impl Pdf2Exporter { let mut residual_dual_dialogue = (None, None); let mut residual_dual_index = (None, None); + let mut outline = Outline::new(); + while screenplay_index < screenplay.elements.len() { let mut page = document .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); @@ -243,7 +254,34 @@ impl Pdf2Exporter { match &element { Element::Heading { slug, number } => { - we(slug, &MARGINS.heading, Alignment::LeftToRight) + if number.is_some() { + let initial_line_index = line_index; + + let left_number_margin = Margin { left: 54.0, right: size.x as }; + + line_index = initial_line_index; + } + outline.push_child(OutlineNode::new( + slug.to_string(), + XyzDestination::new( + page_index, + Point { + x: MARGINS.heading.left, + y: (TOP_MARGIN + (line_index * FONT_SIZE) - FONT_SIZE) as f32, + }, + ), + )); + residual_index = write_element( + size, + slug, + &MARGINS.heading, + &mut breakpoint_index, + &mut line_index, + max_lines, + &mut surface, + fonts, + Alignment::LeftToRight, + ) } Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { @@ -338,7 +376,9 @@ impl Pdf2Exporter { surface.finish(); page.finish(); + page_index += 1; } + document.set_outline(outline); } } diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index db27bf1..d4a7631 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -186,6 +186,15 @@ impl RichString { self.elements.push(Element { text, attributes }); } + + pub fn to_string(&self) -> String { + let mut str = String::with_capacity(self.len()); + for element in &self.elements { + str.push_str(&element.text); + } + + str + } } impl Default for RichString { From cd18e4ae2576c79dc5d6d698c1a73f3a38979ad3 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sun, 1 Mar 2026 00:11:48 +0100 Subject: [PATCH 16/39] Finish working? prototype of layout engine --- crates/rustwell/src/export/pdf2.rs | 61 +++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index fb440c1..4efadb8 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -135,7 +135,7 @@ const MARGINS: Margins = Margins { right: 144.0, }, transition: Margin { - left: 396.0, + left: 144.0, right: 144.0, }, centered: Margin { @@ -257,9 +257,38 @@ impl Pdf2Exporter { if number.is_some() { let initial_line_index = line_index; - let left_number_margin = Margin { left: 54.0, right: size.x as }; + let left_number_margin = Margin { + left: 54.0, + right: size.x as f32 - MARGINS.heading.left + 18.0, + }; + let right_number_margin = Margin { + left: size.x as f32 - MARGINS.heading.right + 18.0, + right: 18.0, + }; + + write_element( + size, + &number.as_ref().unwrap().into(), + &left_number_margin, + &mut 0, + &mut initial_line_index.clone(), + max_lines, + &mut surface, + fonts, + Alignment::LeftToRight, + ); - line_index = initial_line_index; + write_element( + size, + &number.as_ref().unwrap().into(), + &right_number_margin, + &mut 0, + &mut initial_line_index.clone(), + max_lines, + &mut surface, + fonts, + Alignment::RightToLeft, + ); } outline.push_child(OutlineNode::new( slug.to_string(), @@ -528,6 +557,7 @@ fn write_element( fonts, text_direction, size, + margin, ); *breakpoint_index += 1; *line_index += 1; @@ -545,6 +575,7 @@ fn write_line( fonts: &Fonts, text_direction: Alignment, size: &PaperSize, + margin: &Margin, ) { match content.get_char(start_index) { Some(c) => { @@ -560,10 +591,18 @@ fn write_line( None => content.len(), }; - if &text_direction == &Alignment::Centered { - let line_length = breakpoint_index - start_index; - let line_span = (line_length / 2) as f32 * FONT_WIDTH; - x = (size.x / 2) as f32 - line_span; + match text_direction { + Alignment::LeftToRight => (), + Alignment::RightToLeft => { + let line_length = breakpoint_index - start_index; + let line_span = line_length as f32 * FONT_WIDTH; + x += size.x as f32 - (margin.left + margin.right) - line_span; + } + Alignment::Centered => { + let line_length = breakpoint_index - start_index; + let line_span = (line_length / 2) as f32 * FONT_WIDTH; + x = (size.x / 2) as f32 - line_span; + } } let mut glyph_index = 0; @@ -594,19 +633,13 @@ fn write_line( None => string_element.text.len(), }; - let td = match &text_direction { - &Alignment::RightToLeft => krilla::text::TextDirection::RightToLeft, - &Alignment::LeftToRight => krilla::text::TextDirection::LeftToRight, - &Alignment::Centered => krilla::text::TextDirection::LeftToRight, - }; - surface.draw_text( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), font.clone(), FONT_SIZE as f32, &string_element.text[start_byte_index..end_byte_index], false, - td, + krilla::text::TextDirection::LeftToRight, ); glyph_index += relative_break_index - relative_index; From d2c8020c6022520679320085224df89bdf5bff99 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sun, 1 Mar 2026 21:10:08 +0100 Subject: [PATCH 17/39] Rename some variables --- crates/rustwell/src/export/pdf2.rs | 99 +++++++++++++++--------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs index 4efadb8..ca9b85e 100644 --- a/crates/rustwell/src/export/pdf2.rs +++ b/crates/rustwell/src/export/pdf2.rs @@ -201,51 +201,51 @@ impl Pdf2Exporter { screenplay: &Screenplay, fonts: &Fonts, ) { - let mut screenplay_index = 0; + let mut screenplay_element_idx = 0; let mut page_index = 0; - let max_lines = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; - let mut residual_index = None; - let mut residual_dialogue = None; + let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; + let mut residual_breakpoint_idx = None; + let mut residual_dialogue_idx = None; - let mut residual_dual_dialogue = (None, None); - let mut residual_dual_index = (None, None); + let mut residual_dual_dialogue_idx = (None, None); + let mut residual_dual_breakpoint_idx = (None, None); let mut outline = Outline::new(); - while screenplay_index < screenplay.elements.len() { + while screenplay_element_idx < screenplay.elements.len() { let mut page = document .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); let mut surface = page.surface(); - let mut line_index = 0; + let mut line_idx = 0; loop { - if line_index >= max_lines { + if line_idx >= max_lines_per_page { break; } - if screenplay_index >= screenplay.elements.len() { + if screenplay_element_idx >= screenplay.elements.len() { break; } - let element = &screenplay.elements[screenplay_index]; - let mut breakpoint_index = match residual_index { + let element = &screenplay.elements[screenplay_element_idx]; + let mut breakpoint_index = match residual_breakpoint_idx { Some(i) => { - residual_index = None; + residual_breakpoint_idx = None; i } None => 0, }; let mut we = |content, margin, text_direction| { - residual_index = write_element( + residual_breakpoint_idx = write_element( size, content, margin, &mut breakpoint_index, - &mut line_index, - max_lines, + &mut line_idx, + max_lines_per_page, &mut surface, fonts, text_direction, @@ -255,7 +255,7 @@ impl Pdf2Exporter { match &element { Element::Heading { slug, number } => { if number.is_some() { - let initial_line_index = line_index; + let initial_line_index = line_idx; let left_number_margin = Margin { left: 54.0, @@ -272,7 +272,7 @@ impl Pdf2Exporter { &left_number_margin, &mut 0, &mut initial_line_index.clone(), - max_lines, + max_lines_per_page, &mut surface, fonts, Alignment::LeftToRight, @@ -284,7 +284,7 @@ impl Pdf2Exporter { &right_number_margin, &mut 0, &mut initial_line_index.clone(), - max_lines, + max_lines_per_page, &mut surface, fonts, Alignment::RightToLeft, @@ -296,17 +296,17 @@ impl Pdf2Exporter { page_index, Point { x: MARGINS.heading.left, - y: (TOP_MARGIN + (line_index * FONT_SIZE) - FONT_SIZE) as f32, + y: (TOP_MARGIN + (line_idx * FONT_SIZE) - FONT_SIZE) as f32, }, ), )); - residual_index = write_element( + residual_breakpoint_idx = write_element( size, slug, &MARGINS.heading, &mut breakpoint_index, - &mut line_index, - max_lines, + &mut line_idx, + max_lines_per_page, &mut surface, fonts, Alignment::LeftToRight, @@ -316,55 +316,56 @@ impl Pdf2Exporter { Element::Dialogue(dialogue) => { write_dialogue( dialogue, - &mut residual_dialogue, - &mut residual_index, + &mut residual_dialogue_idx, + &mut residual_breakpoint_idx, size, - max_lines, - &mut line_index, + max_lines_per_page, + &mut line_idx, &mut surface, fonts, &MARGINS.dialogue, ); - if residual_dialogue.is_some() { + if residual_dialogue_idx.is_some() { break; } } Element::DualDialogue(dialogue0, dialogue1) => { - let mut initial_line_index = line_index; - if (residual_dual_dialogue.0.is_none() - && residual_dual_dialogue.1.is_none()) - || residual_dual_dialogue.0.is_some() + let mut initial_line_index = line_idx; + if (residual_dual_dialogue_idx.0.is_none() + && residual_dual_dialogue_idx.1.is_none()) + || residual_dual_dialogue_idx.0.is_some() { write_dialogue( dialogue0, - &mut residual_dual_dialogue.0, - &mut residual_dual_index.0, + &mut residual_dual_dialogue_idx.0, + &mut residual_dual_breakpoint_idx.0, size, - max_lines, - &mut line_index, + max_lines_per_page, + &mut line_idx, &mut surface, fonts, &MARGINS.dual_dialogue.left, ); } - if (residual_dual_dialogue.1.is_none() - && residual_dual_dialogue.0.is_none()) - || residual_dual_dialogue.1.is_some() + if (residual_dual_dialogue_idx.1.is_none() + && residual_dual_dialogue_idx.0.is_none()) + || residual_dual_dialogue_idx.1.is_some() { write_dialogue( dialogue1, - &mut residual_dual_dialogue.1, - &mut residual_dual_index.1, + &mut residual_dual_dialogue_idx.1, + &mut residual_dual_breakpoint_idx.1, size, - max_lines, + max_lines_per_page, &mut initial_line_index, &mut surface, fonts, &MARGINS.dual_dialogue.right, ); } - line_index = line_index.max(initial_line_index); - if residual_dual_dialogue.0.is_some() || residual_dual_dialogue.1.is_some() + line_idx = line_idx.max(initial_line_index); + if residual_dual_dialogue_idx.0.is_some() + || residual_dual_dialogue_idx.1.is_some() { break; } @@ -384,8 +385,8 @@ impl Pdf2Exporter { s, &MARGINS.synopsis, &mut breakpoint_index, - &mut line_index, - max_lines, + &mut line_idx, + max_lines_per_page, &mut surface, fonts, Alignment::LeftToRight, @@ -394,13 +395,13 @@ impl Pdf2Exporter { } } Element::PageBreak => { - screenplay_index += 1; + screenplay_element_idx += 1; break; } } - line_index += 1; - screenplay_index += 1; + line_idx += 1; + screenplay_element_idx += 1; } surface.finish(); From 092c93c61c62013b15942bcf601cf7936eb02689 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sun, 1 Mar 2026 21:13:53 +0100 Subject: [PATCH 18/39] Remove old pdf export and typst dependency --- Cargo.lock | 2674 ++--------------------- README.md | 2 - crates/rustwell-cli/src/main.rs | 11 +- crates/rustwell/Cargo.toml | 2 - crates/rustwell/src/export.rs | 2 - crates/rustwell/src/export/pdf.rs | 791 ++++++- crates/rustwell/src/export/pdf2.rs | 796 ------- crates/rustwell/src/export/template.typ | 172 -- crates/rustwell/src/export/typst.rs | 365 ---- crates/rustwell/src/lib.rs | 2 - 10 files changed, 922 insertions(+), 3895 deletions(-) delete mode 100644 crates/rustwell/src/export/pdf2.rs delete mode 100644 crates/rustwell/src/export/template.typ delete mode 100644 crates/rustwell/src/export/typst.rs diff --git a/Cargo.lock b/Cargo.lock index 186569c..0e2196f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,15 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "anstream" version = "0.6.21" @@ -62,7 +53,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -73,25 +64,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object 0.32.2", + "windows-sys", ] [[package]] @@ -112,12 +85,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "az" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" - [[package]] name = "backtrace" version = "0.3.76" @@ -128,7 +95,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object", "rustc-demangle", "windows-link", ] @@ -139,44 +106,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "biblatex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d0c374feba1b9a59042a7c1cf00ce7c34b977b9134fe7c42b08e5183729f66" -dependencies = [ - "paste", - "roman-numerals-rs", - "strum", - "unic-langid", - "unicode-normalization", - "unscanny", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "1.3.2" @@ -188,9 +117,6 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] [[package]] name = "bumpalo" @@ -198,12 +124,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - [[package]] name = "bytemuck" version = "1.24.0" @@ -230,77 +150,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" -[[package]] -name = "cc" -version = "1.2.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chinese-number" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b" -dependencies = [ - "chinese-variant", - "enum-ordinalize", - "num-bigint", - "num-traits", -] - -[[package]] -name = "chinese-variant" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49" - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "citationberg" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6597e8bdbca37f1f56e5a80d15857b0932aead21a78d20de49e99e74933046" -dependencies = [ - "quick-xml", - "serde", -] - [[package]] name = "clap" version = "4.5.53" @@ -341,21 +196,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] - -[[package]] -name = "codex" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9589e1effc5cacbea347899645c654158b03b2053d24bb426fd3128ced6e423c" - [[package]] name = "color-eyre" version = "0.6.5" @@ -395,30 +235,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "comemo" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649d7b2d867b569729c03c0f6968db10bc95921182a1f2b2012b1b549492f39d" -dependencies = [ - "comemo-macros", - "parking_lot", - "rustc-hash", - "siphasher", - "slab", -] - -[[package]] -name = "comemo-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c87fc7e85487493ddedae1a3a34b897c77ad8825375b79265a8a162c28d535" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "core_maths" version = "0.1.1" @@ -437,131 +253,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ecow" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" -dependencies = [ - "serde", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -587,29 +278,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fancy-regex" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "fdeflate" version = "0.3.7" @@ -619,12 +287,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - [[package]] name = "flate2" version = "1.1.5" @@ -636,12 +298,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "float-cmp" version = "0.10.0" @@ -651,12 +307,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "font-types" version = "0.10.1" @@ -666,38 +316,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "gif" version = "0.13.3" @@ -708,39 +326,12 @@ dependencies = [ "weezl", ] -[[package]] -name = "gif" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -[[package]] -name = "glidesort" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -748,407 +339,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "hayagriva" -version = "0.9.1" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb69425736f184173b3ca6e27fcba440a61492a790c786b1c6af7e06a03e575" -dependencies = [ - "biblatex", - "ciborium", - "citationberg", - "indexmap", - "paste", - "roman-numerals-rs", - "serde", - "serde_yaml", - "thiserror", - "unic-langid", - "unicode-segmentation", - "unscanny", - "url", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hayro" -version = "0.4.0" +name = "image-webp" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048488ba88552bb0fb2a7e4001c64d5bed65d1a92167186a1bb9151571f32e60" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ - "bytemuck", - "hayro-interpret", - "image", - "kurbo 0.12.0", + "byteorder-lite", + "quick-error", ] [[package]] -name = "hayro-font" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e7e97ce840a6a70e7901e240ec65ba61106b66b37a4a1b899a2ce484248463" -dependencies = [ - "log", - "phf", -] - -[[package]] -name = "hayro-interpret" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56204c972d08e844f3db13b1e14be769f846e576699b46d4f4637cc4f8f70102" -dependencies = [ - "bitflags 2.10.0", - "hayro-font", - "hayro-syntax", - "kurbo 0.12.0", - "log", - "moxcms", - "phf", - "rustc-hash", - "siphasher", - "skrifa", - "smallvec", - "yoke 0.8.1", -] - -[[package]] -name = "hayro-svg" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c673304cec6e0dfd3b4f71fccecd45646899aa70279b62d3f933842abc4ac5" -dependencies = [ - "base64", - "hayro-interpret", - "image", - "kurbo 0.12.0", - "siphasher", - "xmlwriter", -] - -[[package]] -name = "hayro-syntax" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9e5c7dbc0f11dc42775d1a6cc00f5f5137b90b6288dd7fe5f71d17b14d10be" -dependencies = [ - "flate2", - "kurbo 0.12.0", - "log", - "rustc-hash", - "smallvec", - "zune-jpeg 0.4.21", -] - -[[package]] -name = "hayro-write" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc05d8b4bc878b9aee48d980ecb25ed08f1dd9fad6da5ab4d9b7c56ec03a0cf6" -dependencies = [ - "flate2", - "hayro-syntax", - "log", - "pdf-writer", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hypher" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e25026c579b170c59f8d3ddfc523d7dab0abe079f09eb8edaebd2417044f60" - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "serde", - "yoke 0.7.5", - "zerofrom", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke 0.8.1", - "zerofrom", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap 0.8.1", - "tinystr 0.8.2", - "writeable 0.6.2", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap 0.7.5", - "tinystr 0.7.6", - "writeable 0.5.5", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider 1.5.0", - "tinystr 0.7.6", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections 2.1.1", - "icu_normalizer_data", - "icu_properties 2.1.2", - "icu_provider 2.1.1", - "smallvec", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections 1.5.0", - "icu_locid_transform", - "icu_properties_data 1.5.1", - "icu_provider 1.5.0", - "serde", - "tinystr 0.7.6", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections 2.1.1", - "icu_locale_core", - "icu_properties_data 2.1.2", - "icu_provider 2.1.1", - "zerotrie 0.2.3", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "postcard", - "serde", - "stable_deref_trait", - "tinystr 0.7.6", - "writeable 0.5.5", - "yoke 0.7.5", - "zerofrom", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable 0.6.2", - "yoke 0.8.1", - "zerofrom", - "zerotrie 0.2.3", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_provider_adapters" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6324dfd08348a8e0374a447ebd334044d766b1839bb8d5ccf2482a99a77c0bc" -dependencies = [ - "icu_locid", - "icu_locid_transform", - "icu_provider 1.5.0", - "tinystr 0.7.6", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_provider_blob" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c24b98d1365f55d78186c205817631a4acf08d7a45bdf5dc9dcf9c5d54dccf51" -dependencies = [ - "icu_provider 1.5.0", - "postcard", - "serde", - "writeable 0.5.5", - "zerotrie 0.1.3", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "icu_segmenter" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de" -dependencies = [ - "core_maths", - "displaydoc", - "icu_collections 1.5.0", - "icu_locid", - "icu_provider 1.5.0", - "icu_segmenter_data", - "serde", - "utf8_iter", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_segmenter_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e52775179941363cc594e49ce99284d13d6948928d8e72c755f55e98caa1eb" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties 2.1.2", -] - -[[package]] -name = "image" -version = "0.25.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "gif 0.14.1", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - -[[package]] -name = "imagesize" -version = "0.14.0" +name = "imagesize" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" @@ -1166,16 +374,8 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", - "serde", - "serde_core", ] -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1188,15 +388,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" -[[package]] -name = "kamadak-exif" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" -dependencies = [ - "mutate_once", -] - [[package]] name = "krilla" version = "0.6.0" @@ -1205,18 +396,15 @@ checksum = "a0ddfec86fec13d068075e14f22a7e217c281f3ed69ddcb427bf3f5d504fd674" dependencies = [ "base64", "bumpalo", - "comemo", "flate2", - "float-cmp 0.10.0", - "gif 0.13.3", - "hayro-write", + "float-cmp", + "gif", "image-webp", - "imagesize 0.14.0", + "imagesize", "indexmap", "once_cell", "pdf-writer", - "png 0.17.16", - "rayon", + "png", "rustc-hash", "rustybuzz", "siphasher", @@ -1225,34 +413,8 @@ dependencies = [ "subsetter", "tiny-skia-path", "xmp-writer", - "yoke 0.8.1", - "zune-jpeg 0.5.8", -] - -[[package]] -name = "krilla-svg" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f485e1a850201a01dcd8d73e7cf09f2cd4c4cc85c2cd296359094d49336d8ef7" -dependencies = [ - "flate2", - "fontdb", - "krilla", - "png 0.17.16", - "resvg", - "tiny-skia", - "usvg", -] - -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", + "yoke", + "zune-jpeg", ] [[package]] @@ -1293,46 +455,6 @@ dependencies = [ "zlib-rs", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "lipsum" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" -dependencies = [ - "rand", - "rand_chacha", -] - -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" -dependencies = [ - "serde", -] - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.29" @@ -1345,15 +467,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1365,64 +478,14 @@ dependencies = [ ] [[package]] -name = "moxcms" -version = "0.7.11" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "mutate_once" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "object" version = "0.37.3" @@ -1450,59 +513,6 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" -[[package]] -name = "palette" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" -dependencies = [ - "approx", - "fast-srgb8", - "libm", - "palette_derive", -] - -[[package]] -name = "palette_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" -dependencies = [ - "by_address", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pdf-writer" version = "0.14.0" @@ -1515,80 +525,12 @@ dependencies = [ "ryu", ] -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64", - "indexmap", - "quick-xml", - "serde", - "time", -] - [[package]] name = "png" version = "0.17.16" @@ -1602,67 +544,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.10.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec 0.11.5", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.103" @@ -1672,47 +553,12 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "psm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "pxfm" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qcms" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa" - [[package]] name = "quick-error" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quote" version = "1.0.42" @@ -1722,51 +568,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "read-fonts" version = "0.35.0" @@ -1777,92 +578,6 @@ dependencies = [ "font-types", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "resvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" -dependencies = [ - "gif 0.13.3", - "image-webp", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", - "zune-jpeg 0.4.21", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roman-numerals-rs" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "rust_decimal" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" -dependencies = [ - "arrayvec", - "num-traits", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -1881,8 +596,6 @@ version = "0.1.0" dependencies = [ "bitflags 2.10.0", "krilla", - "typst", - "typst-pdf", ] [[package]] @@ -1913,1163 +626,231 @@ dependencies = [ ] [[package]] -name = "ryu" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp 0.9.0", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "subsetter" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0" -dependencies = [ - "kurbo 0.12.0", - "rustc-hash", - "skrifa", - "write-fonts", -] - -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo 0.11.3", - "siphasher", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syntect" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" -dependencies = [ - "bincode", - "fancy-regex", - "flate2", - "fnv", - "once_cell", - "plist", - "regex-syntax", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "walkdir", - "yaml-rust", -] - -[[package]] -name = "thin-vec" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "serde", - "zerovec 0.10.4", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec 0.11.5", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "two-face" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2" -dependencies = [ - "serde", - "serde_derive", - "syntect", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typst" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6511ee598476f4f322b4d13891083d96dbacb8f9c2b908604c7094ba390653" -dependencies = [ - "comemo", - "ecow", - "rustc-hash", - "typst-eval", - "typst-html", - "typst-layout", - "typst-library", - "typst-macros", - "typst-realize", - "typst-syntax", - "typst-timing", - "typst-utils", -] - -[[package]] -name = "typst-assets" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5613cb719a6222fe9b74027c3625d107767ec187bff26b8fc931cf58942c834f" - -[[package]] -name = "typst-eval" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687757487dfc0c1e941344d5024cf7a28364e70c3e304faad89ac65597f62526" -dependencies = [ - "comemo", - "ecow", - "indexmap", - "rustc-hash", - "stacker", - "toml", - "typst-library", - "typst-macros", - "typst-syntax", - "typst-timing", - "typst-utils", - "unicode-segmentation", -] - -[[package]] -name = "typst-html" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29f8da4f964d4c90739c3c1e0288b0ba1bccc3cc50623a6d558300b86ca8aad" -dependencies = [ - "bumpalo", - "comemo", - "ecow", - "palette", - "rustc-hash", - "time", - "typst-assets", - "typst-library", - "typst-macros", - "typst-svg", - "typst-syntax", - "typst-timing", - "typst-utils", -] - -[[package]] -name = "typst-layout" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cab0200105831a9158e63718a0f6141c78cb2c1722ed17d19ad28941e3b8491" -dependencies = [ - "az", - "bumpalo", - "codex", - "comemo", - "ecow", - "either", - "hypher", - "icu_properties 1.5.1", - "icu_provider 1.5.0", - "icu_provider_adapters", - "icu_provider_blob", - "icu_segmenter", - "kurbo 0.12.0", - "memchr", - "rustc-hash", - "rustybuzz", - "smallvec", - "ttf-parser", - "typst-assets", - "typst-library", - "typst-macros", - "typst-syntax", - "typst-timing", - "typst-utils", - "unicode-bidi", - "unicode-math-class", - "unicode-script", - "unicode-segmentation", -] - -[[package]] -name = "typst-library" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e276a5de53020c43efe2111ec236252e54ea4480b5ac18063e663dfbe03d9d1b" -dependencies = [ - "az", - "bitflags 2.10.0", - "bumpalo", - "chinese-number", - "ciborium", - "codex", - "comemo", - "csv", - "ecow", - "flate2", - "fontdb", - "glidesort", - "hayagriva", - "hayro-syntax", - "icu_properties 1.5.1", - "icu_provider 1.5.0", - "icu_provider_blob", - "image", - "indexmap", - "kamadak-exif", - "kurbo 0.12.0", - "lipsum", - "memchr", - "palette", - "phf", - "png 0.17.16", - "qcms", - "rayon", - "regex", - "regex-syntax", - "roxmltree", - "rust_decimal", - "rustc-hash", - "rustybuzz", - "serde", - "serde_json", - "serde_yaml", - "siphasher", - "smallvec", - "syntect", - "time", - "toml", - "ttf-parser", - "two-face", - "typed-arena", - "typst-assets", - "typst-macros", - "typst-syntax", - "typst-timing", - "typst-utils", - "unicode-math-class", - "unicode-normalization", - "unicode-segmentation", - "unscanny", - "usvg", - "utf8_iter", - "wasmi", - "xmlwriter", -] - -[[package]] -name = "typst-macros" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "141cbd1027129fbf6bda1013f52a264df7befc7388cc8f47767d65e803fd3a59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typst-pdf" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c8a4630754767cd10d48e8b8186e7dc784631a30a3a93521edf7d77aebd0c0" -dependencies = [ - "az", - "bytemuck", - "comemo", - "ecow", - "image", - "indexmap", - "infer", - "krilla", - "krilla-svg", - "rustc-hash", - "serde", - "smallvec", - "typst-assets", - "typst-library", - "typst-macros", - "typst-syntax", - "typst-timing", - "typst-utils", -] - -[[package]] -name = "typst-realize" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ffe964757fb93d2e98978aa2a74ee85b0f94c8643e8f3550737258b58f39d8" -dependencies = [ - "arrayvec", - "bumpalo", - "comemo", - "ecow", - "regex", - "typst-library", - "typst-macros", - "typst-syntax", - "typst-timing", - "typst-utils", -] - -[[package]] -name = "typst-svg" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46b811837ade1f0243ef0d8bf3fb06d166443090eac22c28643f374c2ccdc9d" -dependencies = [ - "base64", - "comemo", - "ecow", - "flate2", - "hayro", - "hayro-svg", - "image", - "rustc-hash", - "ttf-parser", - "typst-assets", - "typst-library", - "typst-macros", - "typst-timing", - "typst-utils", - "xmlparser", - "xmlwriter", -] - -[[package]] -name = "typst-syntax" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a95d9192060e23b1e491b0b94dff676acddc92a4d672aeb8ca3890a5a734e879" -dependencies = [ - "ecow", - "rustc-hash", - "serde", - "toml", - "typst-timing", - "typst-utils", - "unicode-ident", - "unicode-math-class", - "unicode-script", - "unicode-segmentation", - "unscanny", -] - -[[package]] -name = "typst-timing" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be94f8faf19841b49574ef5c7fd7a12e2deb7c3d8deba5a596f35d2222024cd" -dependencies = [ - "parking_lot", - "serde", - "serde_json", -] - -[[package]] -name = "typst-utils" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3966c92e8fa48c7ce898130d07000d985f18206d92b250f0f939287fbccdee3" -dependencies = [ - "once_cell", - "portable-atomic", - "rayon", - "rustc-hash", - "siphasher", - "thin-vec", - "unicode-math-class", -] - -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", - "unic-langid-macros", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "serde", - "tinystr 0.8.2", -] - -[[package]] -name = "unic-langid-macros" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" -dependencies = [ - "proc-macro-hack", - "tinystr 0.8.2", - "unic-langid-impl", - "unic-langid-macros-impl", -] - -[[package]] -name = "unic-langid-macros-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" -dependencies = [ - "proc-macro-hack", - "quote", - "syn", - "unic-langid-impl", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-math-class" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-vo" -version = "0.1.0" +name = "ryu" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" [[package]] -name = "unsafe-libyaml" -version = "0.2.11" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] [[package]] -name = "unscanny" -version = "0.1.0" +name = "simd-adler32" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] -name = "url" -version = "2.5.7" +name = "siphasher" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] -name = "usvg" -version = "0.45.1" +name = "skrifa" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "imagesize 0.13.0", - "kurbo 0.11.3", - "log", - "pico-args", - "roxmltree", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", + "bytemuck", + "read-fonts", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "valuable" +name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" [[package]] -name = "version_check" -version = "0.9.5" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "walkdir" -version = "2.5.0" +name = "subsetter" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0" dependencies = [ - "same-file", - "winapi-util", + "kurbo", + "rustc-hash", + "skrifa", + "write-fonts", ] [[package]] -name = "wasmi" -version = "0.51.5" +name = "syn" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ - "spin", - "wasmi_collections", - "wasmi_core", - "wasmi_ir", - "wasmparser", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "wasmi_collections" -version = "0.51.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b8e98e45a2a534489f8225e765cbf1cb9a3078072605e58158910cf4749172" - -[[package]] -name = "wasmi_core" -version = "0.51.5" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c25f375c0cdf14810eab07f532f61f14d4966f09c747a55067fdf3196e8512e6" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "libm", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasmi_ir" -version = "0.51.5" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624e2a68a4293ecb8f564260b68394b29cf3b3edba6bce35532889a2cb33c3d9" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "wasmi_core", + "cfg-if", ] [[package]] -name = "wasmparser" -version = "0.228.0" +name = "tiny-skia-path" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" dependencies = [ - "bitflags 2.10.0", + "arrayref", + "bytemuck", + "strict-num", ] [[package]] -name = "weezl" -version = "0.1.12" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] [[package]] -name = "winapi-util" -version = "0.1.11" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "windows-sys 0.61.2", + "once_cell", + "valuable", ] [[package]] -name = "windows-link" +name = "tracing-error" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] [[package]] -name = "windows-sys" -version = "0.59.0" +name = "tracing-subscriber" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ - "windows-targets", + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "ttf-parser" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" dependencies = [ - "windows-link", + "core_maths", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "unicode-bidi-mirroring" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "unicode-ccc" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "unicode-ident" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "unicode-properties" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "unicode-script" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "weezl" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "winnow" -version = "0.7.14" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "memchr", + "windows-link", ] [[package]] @@ -3080,62 +861,17 @@ checksum = "886614b5ce857341226aa091f3c285e450683894acaaa7887f366c361efef79d" dependencies = [ "font-types", "indexmap", - "kurbo 0.12.0", + "kurbo", "log", "read-fonts", ] -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "xmp-writer" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - [[package]] name = "yoke" version = "0.8.1" @@ -3143,22 +879,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.1", + "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "yoke-derive" version = "0.8.1" @@ -3171,26 +895,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.6" @@ -3212,113 +916,23 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerotrie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f" -dependencies = [ - "displaydoc", - "litemap 0.7.5", - "serde", - "zerovec 0.10.4", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke 0.8.1", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "serde", - "yoke 0.7.5", - "zerofrom", - "zerovec-derive 0.10.3", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "yoke 0.8.1", - "zerofrom", - "zerovec-derive 0.11.2", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zlib-rs" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" -[[package]] -name = "zmij" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" dependencies = [ - "zune-core 0.5.0", + "zune-core", ] diff --git a/README.md b/README.md index ff387e8..3e7fae1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,4 @@ Rustwell currently implements the entirely of the *Fountain* specification. It o The original CSS code for the html output was created by [Jonathan Poritsky](https://www.candlerblog.com/), but has been expanded upon for this endeavour. -The typst template is based on the [celluloid](https://typst.app/universe/package/celluloid) template for typst by [Casey Dahlin](mailto:sadmac@google.com), licensed under [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) license. The template used for Rustwell is a heavily modified version of celluloid, and is not considered fit for use outside of the scope of Rustwell. The original license can be found [here](./licenses/OFL-Courier-Prime.txt). - [Courier Prime](https://quoteunquoteapps.com/courierprime/), designed by Alan Dague-Greene, is distributed along with Rustwell for the `pdf`-exporter to have a baseline font. The original license can be found [here](./licenses/OFL-Courier-Prime.txt). diff --git a/crates/rustwell-cli/src/main.rs b/crates/rustwell-cli/src/main.rs index e4b3c5d..019e82d 100644 --- a/crates/rustwell-cli/src/main.rs +++ b/crates/rustwell-cli/src/main.rs @@ -4,9 +4,8 @@ use color_eyre::eyre::bail; use rustwell::Exporter; use rustwell::ExporterExt; use rustwell::HtmlExporter; -use rustwell::Pdf2Exporter; +use rustwell::PdfExporter; use rustwell::Screenplay; -use rustwell::TypstExporter; use std::fs::File; use std::io; @@ -39,7 +38,6 @@ struct Cli { #[derive(Debug, Clone, Copy, ValueEnum)] enum Target { - Typst, Html, Pdf, } @@ -74,11 +72,7 @@ fn decide_exporter(cli: &Cli) -> Box { synopses: cli.synopses, ..Default::default() }), - Target::Pdf => Box::new(Pdf2Exporter { - synopses: cli.synopses, - ..Default::default() - }), - Target::Typst => Box::new(TypstExporter { + Target::Pdf => Box::new(PdfExporter { synopses: cli.synopses, ..Default::default() }), @@ -111,7 +105,6 @@ fn detect_target_from_path(path: &str) -> Result { .to_ascii_lowercase(); let t = match ext.as_str() { - "typ" => Target::Typst, "html" | "htm" => Target::Html, "pdf" => Target::Pdf, _ => bail!("unkown extension '.{}'; specify -t/--target", ext), diff --git a/crates/rustwell/Cargo.toml b/crates/rustwell/Cargo.toml index 26b4e59..9ab0061 100644 --- a/crates/rustwell/Cargo.toml +++ b/crates/rustwell/Cargo.toml @@ -14,5 +14,3 @@ path = "src/lib.rs" [dependencies] bitflags = "2" krilla = "0.6.0" -typst = "0.14.2" -typst-pdf = "0.14.2" diff --git a/crates/rustwell/src/export.rs b/crates/rustwell/src/export.rs index 1391993..f1cea35 100644 --- a/crates/rustwell/src/export.rs +++ b/crates/rustwell/src/export.rs @@ -10,8 +10,6 @@ pub mod html; pub mod pdf; -pub mod pdf2; -pub mod typst; use std::{ fs::File, diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 6a56fb5..df10cf6 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -1,18 +1,159 @@ -use std::io::Write; +use std::{io::Write, sync::Arc}; -use typst_pdf::PdfOptions; +use krilla::{ + Document, + color::rgb, + destination::XyzDestination, + geom::Point, + num::NormalizedF32, + outline::{Outline, OutlineNode}, + page::PageSettings, + paint::Fill, + surface::Surface, + text::Font, +}; -use crate::{Exporter, export::typst::TypstExporter, screenplay::Screenplay}; +use crate::{ + Exporter, Screenplay, + rich_string::RichString, + screenplay::{Dialogue, DialogueElement, Element}, +}; + +const FONT_SIZE: usize = 12; +const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) + +/// The font bundled together with Rustwell; Courier Prime. +/// Includes the data of the font styles Regular, Bold, Italic +/// and BoldItalic, in stated order. +const FONTS: [&[u8]; 4] = [ + include_bytes!("fonts/CourierPrime-Regular.ttf"), + include_bytes!("fonts/CourierPrime-Bold.ttf"), + include_bytes!("fonts/CourierPrime-Italic.ttf"), + include_bytes!("fonts/CourierPrime-BoldItalic.ttf"), +]; + +struct Fonts { + pub regular: Font, + pub bold: Font, + pub italic: Font, + pub bold_italic: Font, +} + +pub struct PaperSize { + pub x: usize, + pub y: usize, +} + +pub const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts +pub const LETTER: PaperSize = PaperSize { x: 612, y: 792 }; // Letter size in pts + +const TOP_MARGIN: usize = 72; +const BOTTOM_MARGIN: usize = 72; + +struct Margin { + pub left: f32, + pub right: f32, +} + +struct DialogueMargins { + pub character: Margin, + pub parenthetical: Margin, + pub line: Margin, +} + +struct DualDialogueMargins { + pub left: DialogueMargins, + pub right: DialogueMargins, +} + +struct Margins { + pub heading: Margin, + pub action: Margin, + pub dialogue: DialogueMargins, + pub dual_dialogue: DualDialogueMargins, + pub lyrics: Margin, + pub transition: Margin, + pub centered: Margin, + pub synopsis: Margin, +} + +const MARGINS: Margins = Margins { + heading: Margin { + left: 108.0, + right: 72.0, + }, + action: Margin { + left: 108.0, + right: 72.0, + }, + dialogue: DialogueMargins { + character: Margin { + left: 252.0, + right: 108.0, + }, + parenthetical: Margin { + left: 223.2, + right: 180.0, + }, + line: Margin { + left: 180.0, + right: 144.0, + }, + }, + dual_dialogue: DualDialogueMargins { + left: DialogueMargins { + character: Margin { + left: 198.0, + right: 288.0, + }, + parenthetical: Margin { + left: 162.0, + right: 324.0, + }, + line: Margin { + left: 144.0, + right: 288.0, + }, + }, + right: DialogueMargins { + character: Margin { + left: 414.0, + right: 72.0, + }, + parenthetical: Margin { + left: 378.0, + right: 90.0, + }, + line: Margin { + left: 360.0, + right: 72.0, + }, + }, + }, + lyrics: Margin { + left: 180.0, + right: 144.0, + }, + transition: Margin { + left: 144.0, + right: 144.0, + }, + centered: Margin { + left: 144.0, + right: 144.0, + }, + synopsis: Margin { + left: 108.0, + right: 72.0, + }, +}; /// A [`Screenplay`] exporter for `pdf` /// -/// Uses [`typst`] to create the `pdf` by first exporting the [`Screenplay`] to -/// a [`typst`] document. -/// /// The variables configure the exporter #[derive(Default)] pub struct PdfExporter { - /// Wheter to include synopses in the output + /// Whether to include synopses in the output pub synopses: bool, } @@ -21,15 +162,635 @@ impl Exporter for PdfExporter { "pdf" } - /// Exports a `pdf` file and writes it to the provided writer. This is done by first constructing - /// a [typst] document and then, using [typst], exporting that to an actual `pdf` document. + /// Exports a `pdf` file and writes it to the provided writer. fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> { - let compiled_doc = TypstExporter { - synopses: self.synopses, - } - .compile_document(screenplay)?; - let pdf = typst_pdf::pdf(&compiled_doc, &PdfOptions::default()) - .map_err(|_| std::io::Error::other("failed to create typst pdf"))?; + let regular_data: Arc + Send + Sync> = Arc::new(FONTS[0]); + let bold_data: Arc + Send + Sync> = Arc::new(FONTS[1]); + let italic_data: Arc + Send + Sync> = Arc::new(FONTS[2]); + let bold_italic_data: Arc + Send + Sync> = Arc::new(FONTS[3]); + let mut document = Document::new(); + + let fonts = Fonts { + regular: Font::new(regular_data.into(), 0).unwrap(), + bold: Font::new(bold_data.into(), 0).unwrap(), + italic: Font::new(italic_data.into(), 0).unwrap(), + bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), + }; + + self.generate_pdf(&mut document, &A4, screenplay, &fonts); + + let pdf = document + .finish() + .map_err(|_| std::io::Error::other("failed to create pdf"))?; writer.write_all(&pdf) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum Alignment { + LeftToRight, + RightToLeft, + Centered, +} + +impl PdfExporter { + fn generate_pdf( + &self, + document: &mut Document, + size: &PaperSize, + screenplay: &Screenplay, + fonts: &Fonts, + ) { + let mut screenplay_element_idx = 0; + + let mut page_index = 0; + + let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; + let mut residual_breakpoint_idx = None; + let mut residual_dialogue_idx = None; + + let mut residual_dual_dialogue_idx = (None, None); + let mut residual_dual_breakpoint_idx = (None, None); + + let mut outline = Outline::new(); + + while screenplay_element_idx < screenplay.elements.len() { + let mut page = document + .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); + let mut surface = page.surface(); + let mut line_idx = 0; + + loop { + if line_idx >= max_lines_per_page { + break; + } + + if screenplay_element_idx >= screenplay.elements.len() { + break; + } + + let element = &screenplay.elements[screenplay_element_idx]; + let mut breakpoint_index = match residual_breakpoint_idx { + Some(i) => { + residual_breakpoint_idx = None; + i + } + None => 0, + }; + + let mut we = |content, margin, text_direction| { + residual_breakpoint_idx = write_element( + size, + content, + margin, + &mut breakpoint_index, + &mut line_idx, + max_lines_per_page, + &mut surface, + fonts, + text_direction, + ) + }; + + match &element { + Element::Heading { slug, number } => { + if number.is_some() { + let initial_line_index = line_idx; + + let left_number_margin = Margin { + left: 54.0, + right: size.x as f32 - MARGINS.heading.left + 18.0, + }; + let right_number_margin = Margin { + left: size.x as f32 - MARGINS.heading.right + 18.0, + right: 18.0, + }; + + write_element( + size, + &number.as_ref().unwrap().into(), + &left_number_margin, + &mut 0, + &mut initial_line_index.clone(), + max_lines_per_page, + &mut surface, + fonts, + Alignment::LeftToRight, + ); + + write_element( + size, + &number.as_ref().unwrap().into(), + &right_number_margin, + &mut 0, + &mut initial_line_index.clone(), + max_lines_per_page, + &mut surface, + fonts, + Alignment::RightToLeft, + ); + } + outline.push_child(OutlineNode::new( + slug.to_string(), + XyzDestination::new( + page_index, + Point { + x: MARGINS.heading.left, + y: (TOP_MARGIN + (line_idx * FONT_SIZE) - FONT_SIZE) as f32, + }, + ), + )); + residual_breakpoint_idx = write_element( + size, + slug, + &MARGINS.heading, + &mut breakpoint_index, + &mut line_idx, + max_lines_per_page, + &mut surface, + fonts, + Alignment::LeftToRight, + ) + } + Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), + Element::Dialogue(dialogue) => { + write_dialogue( + dialogue, + &mut residual_dialogue_idx, + &mut residual_breakpoint_idx, + size, + max_lines_per_page, + &mut line_idx, + &mut surface, + fonts, + &MARGINS.dialogue, + ); + if residual_dialogue_idx.is_some() { + break; + } + } + Element::DualDialogue(dialogue0, dialogue1) => { + let mut initial_line_index = line_idx; + if (residual_dual_dialogue_idx.0.is_none() + && residual_dual_dialogue_idx.1.is_none()) + || residual_dual_dialogue_idx.0.is_some() + { + write_dialogue( + dialogue0, + &mut residual_dual_dialogue_idx.0, + &mut residual_dual_breakpoint_idx.0, + size, + max_lines_per_page, + &mut line_idx, + &mut surface, + fonts, + &MARGINS.dual_dialogue.left, + ); + } + if (residual_dual_dialogue_idx.1.is_none() + && residual_dual_dialogue_idx.0.is_none()) + || residual_dual_dialogue_idx.1.is_some() + { + write_dialogue( + dialogue1, + &mut residual_dual_dialogue_idx.1, + &mut residual_dual_breakpoint_idx.1, + size, + max_lines_per_page, + &mut initial_line_index, + &mut surface, + fonts, + &MARGINS.dual_dialogue.right, + ); + } + line_idx = line_idx.max(initial_line_index); + if residual_dual_dialogue_idx.0.is_some() + || residual_dual_dialogue_idx.1.is_some() + { + break; + } + } + Element::Lyrics(s) => we(s, &MARGINS.lyrics, Alignment::RightToLeft), + Element::Transition(s) => we(s, &MARGINS.transition, Alignment::RightToLeft), + Element::CenteredText(s) => we(s, &MARGINS.centered, Alignment::Centered), + Element::Synopsis(s) => { + if self.synopses { + surface.set_fill(Some(Fill { + paint: rgb::Color::new(143, 143, 143).into(), + opacity: NormalizedF32::new(0.5).unwrap(), + rule: Default::default(), + })); + write_element( + size, + s, + &MARGINS.synopsis, + &mut breakpoint_index, + &mut line_idx, + max_lines_per_page, + &mut surface, + fonts, + Alignment::LeftToRight, + ); + surface.set_fill(None); + } + } + Element::PageBreak => { + screenplay_element_idx += 1; + break; + } + } + + line_idx += 1; + screenplay_element_idx += 1; + } + + surface.finish(); + page.finish(); + page_index += 1; + } + document.set_outline(outline); + } +} + +fn write_dialogue( + dialogue: &Dialogue, + residual_dialogue: &mut Option, + residual_index: &mut Option, + size: &PaperSize, + max_lines: usize, + line_index: &mut usize, + surface: &mut Surface, + fonts: &Fonts, + dialogue_margins: &DialogueMargins, +) { + let mut character_name = dialogue.character.clone(); + match (*residual_dialogue, &dialogue.extension) { + (Some(_), _) => { + character_name.append(" (cont'd)".into()); + } + (None, Some(ext)) => { + character_name.append(" (".into()); + character_name.append(ext.clone()); + character_name.append(")".into()); + } + _ => (), + }; + let span = glyph_span( + size, + dialogue_margins.character.left, + dialogue_margins.character.right, + ); + let name_lines_count = break_points(&character_name, span).len() + 1; + if *line_index + name_lines_count + 1 >= max_lines { + return; + } + assert!(name_lines_count < max_lines); + + write_element( + size, + &character_name, + &dialogue_margins.character, + &mut 0, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + ); + + let mut dialogue_index = residual_dialogue.unwrap_or(0); + while dialogue_index < dialogue.elements.len() { + if *line_index + 1 >= max_lines { + *residual_dialogue = Some(dialogue_index); + write_element( + size, + &"(MORE)".into(), + &dialogue_margins.character, + &mut 0, + line_index, + max_lines + 1, + surface, + fonts, + Alignment::LeftToRight, + ); + + return; + } + let mut breakpoint_index = match *residual_index { + Some(i) => { + *residual_index = None; + i + } + None => 0, + }; + + match &dialogue.elements[dialogue_index] { + DialogueElement::Parenthetical(s) => { + *residual_index = write_element( + size, + &s, + &dialogue_margins.parenthetical, + &mut breakpoint_index, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + ) + } + DialogueElement::Line(s) => { + *residual_index = write_element( + size, + &s, + &dialogue_margins.line, + &mut breakpoint_index, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + ) + } + } + + if residual_index.is_some() { + continue; + } + + dialogue_index += 1; + } + + *residual_dialogue = None; +} + +fn write_element( + size: &PaperSize, + content: &RichString, + margin: &Margin, + breakpoint_index: &mut usize, + line_index: &mut usize, + max_lines: usize, + surface: &mut Surface, + fonts: &Fonts, + text_direction: Alignment, +) -> Option { + let left_margin = margin.left; + let right_margin = margin.right; + let span = glyph_span(size, left_margin, right_margin); + let breakpoints = break_points(content, span); + while *breakpoint_index <= breakpoints.len() { + if *line_index >= max_lines { + return Some(*breakpoint_index); + } + + let start_index = if *breakpoint_index == 0 { + 0 + } else { + breakpoints[*breakpoint_index - 1].index + }; + write_line( + surface, + left_margin, + (FONT_SIZE * *line_index + TOP_MARGIN) as f32, + content, + start_index, + breakpoints.get(*breakpoint_index), + fonts, + text_direction, + size, + margin, + ); + *breakpoint_index += 1; + *line_index += 1; + } + None +} + +fn write_line( + surface: &mut Surface, + mut x: f32, + y: f32, + content: &RichString, + mut start_index: usize, + breakpoint: Option<&BreakPoint>, + fonts: &Fonts, + text_direction: Alignment, + size: &PaperSize, + margin: &Margin, +) { + match content.get_char(start_index) { + Some(c) => { + if c == '\n' { + start_index += 1 + } + } + None => todo!(), + } + + let breakpoint_index = match breakpoint { + Some(b) => b.index, + None => content.len(), + }; + + match text_direction { + Alignment::LeftToRight => (), + Alignment::RightToLeft => { + let line_length = breakpoint_index - start_index; + let line_span = line_length as f32 * FONT_WIDTH; + x += size.x as f32 - (margin.left + margin.right) - line_span; + } + Alignment::Centered => { + let line_length = breakpoint_index - start_index; + let line_span = (line_length / 2) as f32 * FONT_WIDTH; + x = (size.x / 2) as f32 - line_span; + } + } + + let mut glyph_index = 0; + while start_index < breakpoint_index { + let (string_element, relative_index) = match content.get_element_from_index(start_index) { + Some(res) => res, + None => todo!(), + }; + + let element_length = string_element.text.chars().count(); + + let relative_break_index = + if breakpoint_index - start_index >= element_length - relative_index { + element_length + } else { + breakpoint_index - (start_index - relative_index) + }; + let font = match (string_element.is_bold(), string_element.is_italic()) { + (false, false) => &fonts.regular, + (true, false) => &fonts.bold, + (false, true) => &fonts.italic, + (true, true) => &fonts.bold_italic, + }; + let mut char_indices = string_element.text.char_indices(); + let start_byte_index = char_indices.nth(relative_index).unwrap().0; + let end_byte_index = match char_indices.nth(relative_break_index - relative_index - 1) { + Some((i, _)) => i, + None => string_element.text.len(), + }; + + surface.draw_text( + Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), + font.clone(), + FONT_SIZE as f32, + &string_element.text[start_byte_index..end_byte_index], + false, + krilla::text::TextDirection::LeftToRight, + ); + + glyph_index += relative_break_index - relative_index; + start_index += relative_break_index - relative_index; + } +} + +fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { + ((size.x as f32 - (left_margin + right_margin)) / FONT_WIDTH) as usize +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +enum BreakType { + NewLine, + BreakWord, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +struct BreakPoint { + pub index: usize, + pub break_type: BreakType, +} + +fn break_points(content: &RichString, span: usize) -> Vec { + debug_assert!(span >= 2); + + let mut brekpoints = Vec::with_capacity(content.len() / span + 1); + let mut last_whitespace_char = (0, 0); + let mut line_len = 0; + for (i, glyph) in content.iter().enumerate() { + line_len += 1; + if glyph == '\n' { + brekpoints.push(BreakPoint { + index: i, + break_type: BreakType::NewLine, + }); + line_len = 0; + continue; + } + + if glyph.is_whitespace() || glyph == '-' { + last_whitespace_char = (brekpoints.len() + 1, i); + continue; + } + + if line_len >= span { + if brekpoints.len() + 1 != last_whitespace_char.0 { + brekpoints.push(BreakPoint { + index: i, + break_type: BreakType::BreakWord, + }); + line_len = 0; + continue; + } + + brekpoints.push(BreakPoint { + index: last_whitespace_char.1 + 1, + break_type: BreakType::NewLine, + }); + line_len = i - last_whitespace_char.1; + } + } + brekpoints +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn breaks_simple() { + let mut rs = RichString::new(); + rs.push_str("hello world"); + + let breakpoints = break_points(&rs, 6); + let correct = vec![BreakPoint { + index: 6, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_simple_with_newline() { + let mut rs = RichString::new(); + rs.push_str("hello\nworld"); + + let breakpoints = break_points(&rs, 100); + let correct = vec![BreakPoint { + index: 5, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_simple_breakword() { + let mut rs = RichString::new(); + rs.push_str("helloworld"); + + let breakpoints = break_points(&rs, 6); + let correct = vec![BreakPoint { + index: 5, + break_type: BreakType::BreakWord, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_simple_utilizing_hyphen() { + let mut rs = RichString::new(); + rs.push_str("hello-world"); + + let breakpoints = break_points(&rs, 7); + let correct = vec![BreakPoint { + index: 6, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_rich() { + let mut rs = RichString::new(); + rs.push_str("he**ll**o wor*ld*"); + + let breakpoints = break_points(&rs, 6); + let correct = vec![BreakPoint { + index: 6, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } + + #[test] + fn breaks_rich_longer() { + let mut rs = RichString::new(); + rs.push_str("Bosse går till **affären** och köper lite mjölk, vilket han tycker är väldigt gott att äta."); + + let breakpoints = break_points(&rs, 60); + let correct = vec![BreakPoint { + index: 56, + break_type: BreakType::NewLine, + }]; + + assert_eq!(breakpoints, correct); + } +} diff --git a/crates/rustwell/src/export/pdf2.rs b/crates/rustwell/src/export/pdf2.rs deleted file mode 100644 index ca9b85e..0000000 --- a/crates/rustwell/src/export/pdf2.rs +++ /dev/null @@ -1,796 +0,0 @@ -use std::{io::Write, sync::Arc}; - -use krilla::{ - Document, - color::rgb, - destination::XyzDestination, - geom::Point, - num::NormalizedF32, - outline::{Outline, OutlineNode}, - page::PageSettings, - paint::Fill, - surface::Surface, - text::Font, -}; - -use crate::{ - Exporter, Screenplay, - rich_string::RichString, - screenplay::{Dialogue, DialogueElement, Element}, -}; - -const FONT_SIZE: usize = 12; -const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) - -/// The font bundled together with Rustwell; Courier Prime. -/// Includes the data of the font styles Regular, Bold, Italic -/// and BoldItalic, in stated order. -const FONTS: [&[u8]; 4] = [ - include_bytes!("fonts/CourierPrime-Regular.ttf"), - include_bytes!("fonts/CourierPrime-Bold.ttf"), - include_bytes!("fonts/CourierPrime-Italic.ttf"), - include_bytes!("fonts/CourierPrime-BoldItalic.ttf"), -]; - -struct Fonts { - pub regular: Font, - pub bold: Font, - pub italic: Font, - pub bold_italic: Font, -} - -pub struct PaperSize { - pub x: usize, - pub y: usize, -} - -pub const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts -pub const LETTER: PaperSize = PaperSize { x: 612, y: 792 }; // Letter size in pts - -const TOP_MARGIN: usize = 72; -const BOTTOM_MARGIN: usize = 72; - -struct Margin { - pub left: f32, - pub right: f32, -} - -struct DialogueMargins { - pub character: Margin, - pub parenthetical: Margin, - pub line: Margin, -} - -struct DualDialogueMargins { - pub left: DialogueMargins, - pub right: DialogueMargins, -} - -struct Margins { - pub heading: Margin, - pub action: Margin, - pub dialogue: DialogueMargins, - pub dual_dialogue: DualDialogueMargins, - pub lyrics: Margin, - pub transition: Margin, - pub centered: Margin, - pub synopsis: Margin, -} - -const MARGINS: Margins = Margins { - heading: Margin { - left: 108.0, - right: 72.0, - }, - action: Margin { - left: 108.0, - right: 72.0, - }, - dialogue: DialogueMargins { - character: Margin { - left: 252.0, - right: 108.0, - }, - parenthetical: Margin { - left: 223.2, - right: 180.0, - }, - line: Margin { - left: 180.0, - right: 144.0, - }, - }, - dual_dialogue: DualDialogueMargins { - left: DialogueMargins { - character: Margin { - left: 198.0, - right: 288.0, - }, - parenthetical: Margin { - left: 162.0, - right: 324.0, - }, - line: Margin { - left: 144.0, - right: 288.0, - }, - }, - right: DialogueMargins { - character: Margin { - left: 414.0, - right: 72.0, - }, - parenthetical: Margin { - left: 378.0, - right: 90.0, - }, - line: Margin { - left: 360.0, - right: 72.0, - }, - }, - }, - lyrics: Margin { - left: 180.0, - right: 144.0, - }, - transition: Margin { - left: 144.0, - right: 144.0, - }, - centered: Margin { - left: 144.0, - right: 144.0, - }, - synopsis: Margin { - left: 108.0, - right: 72.0, - }, -}; - -/// A [`Screenplay`] exporter for `pdf` -/// -/// The variables configure the exporter -#[derive(Default)] -pub struct Pdf2Exporter { - /// Whether to include synopses in the output - pub synopses: bool, -} - -impl Exporter for Pdf2Exporter { - fn file_extension(&self) -> &'static str { - "pdf" - } - - /// Exports a `pdf` file and writes it to the provided writer. - fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> { - let regular_data: Arc + Send + Sync> = Arc::new(FONTS[0]); - let bold_data: Arc + Send + Sync> = Arc::new(FONTS[1]); - let italic_data: Arc + Send + Sync> = Arc::new(FONTS[2]); - let bold_italic_data: Arc + Send + Sync> = Arc::new(FONTS[3]); - let mut document = Document::new(); - - let fonts = Fonts { - regular: Font::new(regular_data.into(), 0).unwrap(), - bold: Font::new(bold_data.into(), 0).unwrap(), - italic: Font::new(italic_data.into(), 0).unwrap(), - bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), - }; - - self.generate_pdf(&mut document, &A4, screenplay, &fonts); - - let pdf = document - .finish() - .map_err(|_| std::io::Error::other("failed to create pdf"))?; - writer.write_all(&pdf) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum Alignment { - LeftToRight, - RightToLeft, - Centered, -} - -impl Pdf2Exporter { - fn generate_pdf( - &self, - document: &mut Document, - size: &PaperSize, - screenplay: &Screenplay, - fonts: &Fonts, - ) { - let mut screenplay_element_idx = 0; - - let mut page_index = 0; - - let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; - let mut residual_breakpoint_idx = None; - let mut residual_dialogue_idx = None; - - let mut residual_dual_dialogue_idx = (None, None); - let mut residual_dual_breakpoint_idx = (None, None); - - let mut outline = Outline::new(); - - while screenplay_element_idx < screenplay.elements.len() { - let mut page = document - .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); - let mut surface = page.surface(); - let mut line_idx = 0; - - loop { - if line_idx >= max_lines_per_page { - break; - } - - if screenplay_element_idx >= screenplay.elements.len() { - break; - } - - let element = &screenplay.elements[screenplay_element_idx]; - let mut breakpoint_index = match residual_breakpoint_idx { - Some(i) => { - residual_breakpoint_idx = None; - i - } - None => 0, - }; - - let mut we = |content, margin, text_direction| { - residual_breakpoint_idx = write_element( - size, - content, - margin, - &mut breakpoint_index, - &mut line_idx, - max_lines_per_page, - &mut surface, - fonts, - text_direction, - ) - }; - - match &element { - Element::Heading { slug, number } => { - if number.is_some() { - let initial_line_index = line_idx; - - let left_number_margin = Margin { - left: 54.0, - right: size.x as f32 - MARGINS.heading.left + 18.0, - }; - let right_number_margin = Margin { - left: size.x as f32 - MARGINS.heading.right + 18.0, - right: 18.0, - }; - - write_element( - size, - &number.as_ref().unwrap().into(), - &left_number_margin, - &mut 0, - &mut initial_line_index.clone(), - max_lines_per_page, - &mut surface, - fonts, - Alignment::LeftToRight, - ); - - write_element( - size, - &number.as_ref().unwrap().into(), - &right_number_margin, - &mut 0, - &mut initial_line_index.clone(), - max_lines_per_page, - &mut surface, - fonts, - Alignment::RightToLeft, - ); - } - outline.push_child(OutlineNode::new( - slug.to_string(), - XyzDestination::new( - page_index, - Point { - x: MARGINS.heading.left, - y: (TOP_MARGIN + (line_idx * FONT_SIZE) - FONT_SIZE) as f32, - }, - ), - )); - residual_breakpoint_idx = write_element( - size, - slug, - &MARGINS.heading, - &mut breakpoint_index, - &mut line_idx, - max_lines_per_page, - &mut surface, - fonts, - Alignment::LeftToRight, - ) - } - Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), - Element::Dialogue(dialogue) => { - write_dialogue( - dialogue, - &mut residual_dialogue_idx, - &mut residual_breakpoint_idx, - size, - max_lines_per_page, - &mut line_idx, - &mut surface, - fonts, - &MARGINS.dialogue, - ); - if residual_dialogue_idx.is_some() { - break; - } - } - Element::DualDialogue(dialogue0, dialogue1) => { - let mut initial_line_index = line_idx; - if (residual_dual_dialogue_idx.0.is_none() - && residual_dual_dialogue_idx.1.is_none()) - || residual_dual_dialogue_idx.0.is_some() - { - write_dialogue( - dialogue0, - &mut residual_dual_dialogue_idx.0, - &mut residual_dual_breakpoint_idx.0, - size, - max_lines_per_page, - &mut line_idx, - &mut surface, - fonts, - &MARGINS.dual_dialogue.left, - ); - } - if (residual_dual_dialogue_idx.1.is_none() - && residual_dual_dialogue_idx.0.is_none()) - || residual_dual_dialogue_idx.1.is_some() - { - write_dialogue( - dialogue1, - &mut residual_dual_dialogue_idx.1, - &mut residual_dual_breakpoint_idx.1, - size, - max_lines_per_page, - &mut initial_line_index, - &mut surface, - fonts, - &MARGINS.dual_dialogue.right, - ); - } - line_idx = line_idx.max(initial_line_index); - if residual_dual_dialogue_idx.0.is_some() - || residual_dual_dialogue_idx.1.is_some() - { - break; - } - } - Element::Lyrics(s) => we(s, &MARGINS.lyrics, Alignment::RightToLeft), - Element::Transition(s) => we(s, &MARGINS.transition, Alignment::RightToLeft), - Element::CenteredText(s) => we(s, &MARGINS.centered, Alignment::Centered), - Element::Synopsis(s) => { - if self.synopses { - surface.set_fill(Some(Fill { - paint: rgb::Color::new(143, 143, 143).into(), - opacity: NormalizedF32::new(0.5).unwrap(), - rule: Default::default(), - })); - write_element( - size, - s, - &MARGINS.synopsis, - &mut breakpoint_index, - &mut line_idx, - max_lines_per_page, - &mut surface, - fonts, - Alignment::LeftToRight, - ); - surface.set_fill(None); - } - } - Element::PageBreak => { - screenplay_element_idx += 1; - break; - } - } - - line_idx += 1; - screenplay_element_idx += 1; - } - - surface.finish(); - page.finish(); - page_index += 1; - } - document.set_outline(outline); - } -} - -fn write_dialogue( - dialogue: &Dialogue, - residual_dialogue: &mut Option, - residual_index: &mut Option, - size: &PaperSize, - max_lines: usize, - line_index: &mut usize, - surface: &mut Surface, - fonts: &Fonts, - dialogue_margins: &DialogueMargins, -) { - let mut character_name = dialogue.character.clone(); - match (*residual_dialogue, &dialogue.extension) { - (Some(_), _) => { - character_name.append(" (cont'd)".into()); - } - (None, Some(ext)) => { - character_name.append(" (".into()); - character_name.append(ext.clone()); - character_name.append(")".into()); - } - _ => (), - }; - let span = glyph_span( - size, - dialogue_margins.character.left, - dialogue_margins.character.right, - ); - let name_lines_count = break_points(&character_name, span).len() + 1; - if *line_index + name_lines_count + 1 >= max_lines { - return; - } - assert!(name_lines_count < max_lines); - - write_element( - size, - &character_name, - &dialogue_margins.character, - &mut 0, - line_index, - max_lines, - surface, - fonts, - Alignment::LeftToRight, - ); - - let mut dialogue_index = residual_dialogue.unwrap_or(0); - while dialogue_index < dialogue.elements.len() { - if *line_index + 1 >= max_lines { - *residual_dialogue = Some(dialogue_index); - write_element( - size, - &"(MORE)".into(), - &dialogue_margins.character, - &mut 0, - line_index, - max_lines + 1, - surface, - fonts, - Alignment::LeftToRight, - ); - - return; - } - let mut breakpoint_index = match *residual_index { - Some(i) => { - *residual_index = None; - i - } - None => 0, - }; - - match &dialogue.elements[dialogue_index] { - DialogueElement::Parenthetical(s) => { - *residual_index = write_element( - size, - &s, - &dialogue_margins.parenthetical, - &mut breakpoint_index, - line_index, - max_lines, - surface, - fonts, - Alignment::LeftToRight, - ) - } - DialogueElement::Line(s) => { - *residual_index = write_element( - size, - &s, - &dialogue_margins.line, - &mut breakpoint_index, - line_index, - max_lines, - surface, - fonts, - Alignment::LeftToRight, - ) - } - } - - if residual_index.is_some() { - continue; - } - - dialogue_index += 1; - } - - *residual_dialogue = None; -} - -fn write_element( - size: &PaperSize, - content: &RichString, - margin: &Margin, - breakpoint_index: &mut usize, - line_index: &mut usize, - max_lines: usize, - surface: &mut Surface, - fonts: &Fonts, - text_direction: Alignment, -) -> Option { - let left_margin = margin.left; - let right_margin = margin.right; - let span = glyph_span(size, left_margin, right_margin); - let breakpoints = break_points(content, span); - while *breakpoint_index <= breakpoints.len() { - if *line_index >= max_lines { - return Some(*breakpoint_index); - } - - let start_index = if *breakpoint_index == 0 { - 0 - } else { - breakpoints[*breakpoint_index - 1].index - }; - write_line( - surface, - left_margin, - (FONT_SIZE * *line_index + TOP_MARGIN) as f32, - content, - start_index, - breakpoints.get(*breakpoint_index), - fonts, - text_direction, - size, - margin, - ); - *breakpoint_index += 1; - *line_index += 1; - } - None -} - -fn write_line( - surface: &mut Surface, - mut x: f32, - y: f32, - content: &RichString, - mut start_index: usize, - breakpoint: Option<&BreakPoint>, - fonts: &Fonts, - text_direction: Alignment, - size: &PaperSize, - margin: &Margin, -) { - match content.get_char(start_index) { - Some(c) => { - if c == '\n' { - start_index += 1 - } - } - None => todo!(), - } - - let breakpoint_index = match breakpoint { - Some(b) => b.index, - None => content.len(), - }; - - match text_direction { - Alignment::LeftToRight => (), - Alignment::RightToLeft => { - let line_length = breakpoint_index - start_index; - let line_span = line_length as f32 * FONT_WIDTH; - x += size.x as f32 - (margin.left + margin.right) - line_span; - } - Alignment::Centered => { - let line_length = breakpoint_index - start_index; - let line_span = (line_length / 2) as f32 * FONT_WIDTH; - x = (size.x / 2) as f32 - line_span; - } - } - - let mut glyph_index = 0; - while start_index < breakpoint_index { - let (string_element, relative_index) = match content.get_element_from_index(start_index) { - Some(res) => res, - None => todo!(), - }; - - let element_length = string_element.text.chars().count(); - - let relative_break_index = - if breakpoint_index - start_index >= element_length - relative_index { - element_length - } else { - breakpoint_index - (start_index - relative_index) - }; - let font = match (string_element.is_bold(), string_element.is_italic()) { - (false, false) => &fonts.regular, - (true, false) => &fonts.bold, - (false, true) => &fonts.italic, - (true, true) => &fonts.bold_italic, - }; - let mut char_indices = string_element.text.char_indices(); - let start_byte_index = char_indices.nth(relative_index).unwrap().0; - let end_byte_index = match char_indices.nth(relative_break_index - relative_index - 1) { - Some((i, _)) => i, - None => string_element.text.len(), - }; - - surface.draw_text( - Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), - font.clone(), - FONT_SIZE as f32, - &string_element.text[start_byte_index..end_byte_index], - false, - krilla::text::TextDirection::LeftToRight, - ); - - glyph_index += relative_break_index - relative_index; - start_index += relative_break_index - relative_index; - } -} - -fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { - ((size.x as f32 - (left_margin + right_margin)) / FONT_WIDTH) as usize -} - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -enum BreakType { - NewLine, - BreakWord, -} - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -struct BreakPoint { - pub index: usize, - pub break_type: BreakType, -} - -fn break_points(content: &RichString, span: usize) -> Vec { - debug_assert!(span >= 2); - - let mut brekpoints = Vec::with_capacity(content.len() / span + 1); - let mut last_whitespace_char = (0, 0); - let mut line_len = 0; - for (i, glyph) in content.iter().enumerate() { - line_len += 1; - if glyph == '\n' { - brekpoints.push(BreakPoint { - index: i, - break_type: BreakType::NewLine, - }); - line_len = 0; - continue; - } - - if glyph.is_whitespace() || glyph == '-' { - last_whitespace_char = (brekpoints.len() + 1, i); - continue; - } - - if line_len >= span { - if brekpoints.len() + 1 != last_whitespace_char.0 { - brekpoints.push(BreakPoint { - index: i, - break_type: BreakType::BreakWord, - }); - line_len = 0; - continue; - } - - brekpoints.push(BreakPoint { - index: last_whitespace_char.1 + 1, - break_type: BreakType::NewLine, - }); - line_len = i - last_whitespace_char.1; - } - } - brekpoints -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn breaks_simple() { - let mut rs = RichString::new(); - rs.push_str("hello world"); - - let breakpoints = break_points(&rs, 6); - let correct = vec![BreakPoint { - index: 6, - break_type: BreakType::NewLine, - }]; - - assert_eq!(breakpoints, correct); - } - - #[test] - fn breaks_simple_with_newline() { - let mut rs = RichString::new(); - rs.push_str("hello\nworld"); - - let breakpoints = break_points(&rs, 100); - let correct = vec![BreakPoint { - index: 5, - break_type: BreakType::NewLine, - }]; - - assert_eq!(breakpoints, correct); - } - - #[test] - fn breaks_simple_breakword() { - let mut rs = RichString::new(); - rs.push_str("helloworld"); - - let breakpoints = break_points(&rs, 6); - let correct = vec![BreakPoint { - index: 5, - break_type: BreakType::BreakWord, - }]; - - assert_eq!(breakpoints, correct); - } - - #[test] - fn breaks_simple_utilizing_hyphen() { - let mut rs = RichString::new(); - rs.push_str("hello-world"); - - let breakpoints = break_points(&rs, 7); - let correct = vec![BreakPoint { - index: 6, - break_type: BreakType::NewLine, - }]; - - assert_eq!(breakpoints, correct); - } - - #[test] - fn breaks_rich() { - let mut rs = RichString::new(); - rs.push_str("he**ll**o wor*ld*"); - - let breakpoints = break_points(&rs, 6); - let correct = vec![BreakPoint { - index: 6, - break_type: BreakType::NewLine, - }]; - - assert_eq!(breakpoints, correct); - } - - #[test] - fn breaks_rich_longer() { - let mut rs = RichString::new(); - rs.push_str("Bosse går till **affären** och köper lite mjölk, vilket han tycker är väldigt gott att äta."); - - let breakpoints = break_points(&rs, 60); - let correct = vec![BreakPoint { - index: 56, - break_type: BreakType::NewLine, - }]; - - assert_eq!(breakpoints, correct); - } -} diff --git a/crates/rustwell/src/export/template.typ b/crates/rustwell/src/export/template.typ deleted file mode 100644 index 15c60f4..0000000 --- a/crates/rustwell/src/export/template.typ +++ /dev/null @@ -1,172 +0,0 @@ -// This template is directly based on the 'celluloid' typst template -// by Casey Dahlin. See https://typst.app/universe/package/celluloid for -// the original template. This template has been heavily modified and is -// not fit for use outside of Rustwell, we instead refer to using the -// fantastic celluloid template for writing screenplays in typst. Below -// is the original copyright notice from celluloid. - -// Copyright (C) 2025 Casey Dahlin -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#let line_spacing(blanks) = 0.65em + blanks * 1.23em -#let dialogue_counter = counter("dialogue") - -#let screenplay( - titlepage: false, - title: none, - credit: none, - authors: none, - source: none, - draft_date: none, - contact: none, - doc, -) = { - set page( - margin: ( top: 1in, left: 1.5in, right: 1in, bottom: 0.5in), - header-ascent: 0.5in, - ) - set text( - font: "Courier Prime", - size: 12pt, - ) - set par(spacing: line_spacing(1)) - - show heading: h => { - set text(size: 12pt, weight: "regular") - set block(spacing: line_spacing(1)) - h - } - - if titlepage { - page({ - align(center, { - upper(title) - - if credit != none { - block(credit, above: 1in) - } - if authors != none { - block(authors, spacing: 0.5in) - } - if source != none { - block(source, spacing: 0.6in) - } - if draft_date != none { - block(draft_date, spacing: 0.9in) - } - }) - - if contact != none { - align(bottom, box(align(left, contact))) - } - counter(page).update(0) - }, margin: ( top: 2in, bottom: 1in )) - } - - set page( - header: context { - if counter(page).get().at(0) > 0 { - align(right, counter(page).display("1.")) - } - }, - ) - - doc -} - -#let dialogue_raw(character, dialogue, paren: none, left_inset, left_inset_name, right_inset_name) = { - context { - set par(spacing: line_spacing(0)) - dialogue_counter.step() - let dialogue_count = dialogue_counter.get().at(0) - let dialogue_header_counter = counter("dialogue_header" + str(dialogue_count)) - let dialogue_footer_counter = counter("dialogue_footer" + str(dialogue_count)) - grid( - grid.header(block(par([#upper(context { - dialogue_header_counter.step() - let paren = if dialogue_header_counter.get() != (0,) { - "CONT’D" - } else { - paren - } - character - if paren != none { - [ (#paren)] - } - })]), inset: (left: left_inset))), - block(dialogue, spacing: line_spacing(0)), - grid.footer(block({ - dialogue_footer_counter.step() - context { - if dialogue_footer_counter.get() != dialogue_footer_counter.final() { - [(MORE)] - } - } - }, inset: (left: left_inset), spacing: line_spacing(0))), - inset: (left: left_inset_name, right: right_inset_name), - gutter: line_spacing(0), - ) - } -} - -#let dialogue(character, dialogue, paren: none) = dialogue_raw(character, dialogue, paren: paren, 1.5in, 1in, 1.5in) - -#let dual_dialogue(character1, dialogue1, paren1: none, character2, dialogue2, paren2: none) = grid( - columns: 2, - dialogue_raw(character1, dialogue1, paren: paren1, 1in, 0.5in, 1in), - dialogue_raw(character2, dialogue2, paren: paren2, 1in, 0.5in, 1in), -) - -#let lyrics(cont) = { - block( - upper(cont), - above: line_spacing(0), - inset: (left: 1in, right: 1.5in), - ) -} - -#let parenthetical(content) = { - context{ - block( - par([#content], hanging-indent: measure("(").width), - inset: (left: 0.5in, right: 1in), spacing: line_spacing(0) - ) - } -} - -#let scene(cont, number: none) = { - grid( - columns: (0em, 1fr, 0em), - place(dx: -3em, align(right, number)), - heading(block(upper(cont), above: line_spacing(2), below: line_spacing(1))), - place(dx: 1em, number), - ) -} - -#let centered(cont) = { - block({ - align(center, block(cont)) - }, breakable: false, width: 100%, below: line_spacing(2)) -} - -#let transition(name) = { - align(right, block([#upper(name)], spacing: line_spacing(1), inset: (right: 2em))) -} - -#let synopsis(cont) = { - block( - inset: (left: 1em, right: 1em), - text(fill: luma(100), cont) - ) -} diff --git a/crates/rustwell/src/export/typst.rs b/crates/rustwell/src/export/typst.rs deleted file mode 100644 index a51234a..0000000 --- a/crates/rustwell/src/export/typst.rs +++ /dev/null @@ -1,365 +0,0 @@ -use std::{collections::HashMap, io::Write}; - -use typst::{ - self, Library, LibraryExt, - diag::{FileError, FileResult}, - foundations::{Bytes, Datetime}, - layout::PagedDocument, - syntax::{FileId, Source, VirtualPath}, - text::{Font, FontBook, FontInfo}, - utils::LazyHash, -}; - -use crate::{ - Exporter, - rich_string::{self, RichString}, - screenplay::{DialogueElement, Element, Screenplay}, -}; - -/// The contents of the [typst] template `template.typ` found in the -/// export module. -const TEMPLATE: &str = include_str!("template.typ"); - -/// A [`Screenplay`] exporter for `typst` -/// -/// The [`Exporter`] implementation for [`Exporter::export`] exports [`typst`] -/// source code. -/// -/// The variables configure the exporter -#[derive(Default)] -pub struct TypstExporter { - /// If synopses should be included in the output - pub synopses: bool, -} - -impl Exporter for TypstExporter { - fn file_extension(&self) -> &'static str { - "typ" - } - - /// Exports [`typst`] source code - fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> { - self.export_typst(screenplay, writer) - } -} - -impl TypstExporter { - /// Exports the provided [Screenplay] as a pure [typst] document that can be - /// manually compiled with any [typst]-compiler. The document will not be very - /// readable nor be provided with comments explaining anything. This is mainly included - /// for debugging. - pub fn export_typst( - &self, - screenplay: &Screenplay, - mut writer: impl Write, - ) -> std::io::Result<()> { - let content = self.format_as_typst(screenplay); - write!(writer, "{content}") - } - - /// Generates a [PagedDocument], which is a layouted [typst] document which can then - /// be exported and written with any [typst] exporter, like [typst_pdf]. - pub fn compile_document(&self, screenplay: &Screenplay) -> std::io::Result { - let (fontbook, fonts) = create_fontbook(); - let content = self.format_as_typst(screenplay); - let worldplay = WorldPlay::new(content, &fontbook, &fonts); - let pd: Result = typst::compile(&worldplay).output; - match pd { - Ok(p) => Ok(p), - Err(_) => Err(std::io::Error::other("failed to compile typst document")), - } - } - - /// Formats the [Screenplay] as a [typst] document, meaning it essentially gets - /// converted into [typst]-compilable code. - fn format_as_typst(&self, screenplay: &Screenplay) -> String { - let formatted_elements = screenplay - .elements - .iter() - .map(|e| self.export_element(e)) - .collect::>(); - let titlepage = self.export_titlepage(screenplay); - format!("{TEMPLATE}\n{titlepage}\n{}", formatted_elements.join("\n")) - } - - /// Exports the [crate::screenplay::TitlePage] in the provided [Screenplay] to [typst] code. - /// This function also provides the necessary `#show: screenplay.with(...)` that - /// handles the page layout for the whole screenplay. - fn export_titlepage(&self, screenplay: &Screenplay) -> String { - if let Some(titlepage) = &screenplay.titlepage { - let title = self.format_titlepage_element(&titlepage.title); - let credit = self.format_titlepage_element(&titlepage.credit); - let authors = self.format_titlepage_element(&titlepage.authors); - let source = self.format_titlepage_element(&titlepage.source); - let draft_date = self.format_titlepage_element(&titlepage.draft_date); - let contact = self.format_titlepage_element(&titlepage.contact); - format!( - r#"#show: screenplay.with( - titlepage: true, - title: {title}, - credit: {credit}, - authors: {authors}, - source: {source}, - draft_date: {draft_date}, - contact: {contact}, -)"# - ) - } else { - "#show: screenplay.with(titlepage: false)".to_string() - } - } - - /// Exports a single [Element] as [typst] code. Primarily done by calling the associated - /// [typst] function found in the template. - fn export_element(&self, element: &Element) -> String { - match element { - Element::Heading { slug, number } => { - if let Some(num) = number { - format!( - r#"#scene(number: "{}")[{}]"#, - self.replace_escaping(num), - self.format_rich_string(slug) - ) - } else { - format!("#scene[{}]", self.format_rich_string(slug)) - } - } - Element::Action(s) => self.format_rich_string(s), - Element::Dialogue(dialogue) => format!( - "#dialogue(paren: {})[{}][{}]", - self.format_character_extension(&dialogue.extension), - self.format_rich_string(&dialogue.character), - self.format_dialogue(&dialogue.elements), - ), - Element::DualDialogue(dialogue1, dialogue2) => format!( - "#dual_dialogue(paren1: {}, paren2: {})[{}][{}][{}][{}]", - self.format_character_extension(&dialogue1.extension), - self.format_character_extension(&dialogue2.extension), - self.format_rich_string(&dialogue1.character), - self.format_dialogue(&dialogue1.elements), - self.format_rich_string(&dialogue2.character), - self.format_dialogue(&dialogue2.elements), - ), - Element::Lyrics(s) => format!("#lyrics[{}]", self.format_rich_string(s)), - Element::Transition(s) => format!("#transition[{}]", self.format_rich_string(s)), - Element::CenteredText(s) => format!("#centered[{}]", self.format_rich_string(s)), - Element::Synopsis(s) => { - if self.synopses { - format!("#synopsis[{}]", self.format_rich_string(s)) - } else { - "".to_string() - } - } - Element::PageBreak => "#pagebreak()".to_string(), - } - } - - /// Formats the dialogue into [typst] code. - fn format_dialogue(&self, dialogue: &[DialogueElement]) -> String { - dialogue - .iter() - .map(|d| self.format_dialogue_element(d)) - .collect::>() - .join(" ") - } - - /// Formats the character extension (`(V.O)`, for example) that is - /// next to a character's name in a dialogue. - fn format_character_extension(&self, opt_ext: &Option) -> String { - if let Some(ext) = opt_ext { - format!("[{}]", self.format_rich_string(ext)) - } else { - "none".to_string() - } - } - - /// Formats a [DialogueElement] into a [typst] code. - fn format_dialogue_element(&self, element: &DialogueElement) -> String { - match element { - DialogueElement::Parenthetical(s) => { - format!("#parenthetical[{}]", self.format_rich_string(s)) - } - DialogueElement::Line(s) => self.format_rich_string(s), - } - } - - /// Formats a [RichString] into a [typst]-[String]. - fn format_rich_string(&self, str: &RichString) -> String { - str.elements - .iter() - .map(|e| self.format_rich_element(e)) - .collect::>() - .concat() - } - - /// Formats a [RichString] [rich_string::Element] into a [typst]-[String]. - /// All elements will be explicitly contained in a `#text("{element.text}")` - /// function from [typst], with styling using `weight: "bold"`, `style: "italic"` - /// and `#underline[#text(...)]`. - /// - /// This function also iterates over each string twice to replace all escaping - /// characters `\` and `"` with `\\` and `\*` respectively. - fn format_rich_element(&self, element: &rich_string::Element) -> String { - // Assumes newlines '\n' will only occur sole elements - if element.text == "\n" { - return "\\ ".to_string(); - } - - let mut out = format!( - "#text({}{}\"{}\")", - if element.is_bold() { - "weight: \"bold\"," - } else { - "" - }, - if element.is_italic() { - "style: \"italic\"," - } else { - "" - }, - self.replace_escaping(&element.text) - ); - if element.is_underline() { - out = format!("#underline[{}]", out); - } - - out - } - - /// This function also iterates over each string twice to replace all escaping - /// characters `\` and `"` with `\\` and `\*` respectively. - fn replace_escaping(&self, s: &str) -> String { - s.replace("\\", "\\\\").replace("\"", "\\\"") - } - - /// Formats a single [crate::screenplay::TitlePage] element into [typst] code. - /// If no value has been declared it will return `"none"`. - fn format_titlepage_element(&self, element: &[RichString]) -> String { - if element.is_empty() { - return "none".to_string(); - } - format!( - "[{}]", - element - .iter() - .map(|e| self.format_rich_string(e)) - .collect::>() - .join("\\ ") - ) - } -} - -/// Internal [typst::World] which is basically the whole underlying structure of the [typst] -/// document. This is significantly more slimmed down than a real [typst::World] is, as -/// everything not needed for Rustwell has been stripped away. -struct WorldPlay<'a> { - library: LazyHash, - book: &'a LazyHash, - source: HashMap, - fonts: &'a Vec, -} - -/// `MAIN` contains the "filename" of the main file, which in [typst] **has** to be `/main.typ`. -const MAIN: &str = "/main.typ"; - -/// The font bundled together with Rustwell; Courier Prime. -/// Includes the data of the font styles Regular, Bold, Italic -/// and BoldItalic, in stated order. -const FONTS: [&[u8]; 4] = [ - include_bytes!("fonts/CourierPrime-Regular.ttf"), - include_bytes!("fonts/CourierPrime-Bold.ttf"), - include_bytes!("fonts/CourierPrime-Italic.ttf"), - include_bytes!("fonts/CourierPrime-BoldItalic.ttf"), -]; - -impl<'a> WorldPlay<'a> { - fn new(content: String, book: &'a LazyHash, fonts: &'a Vec) -> Self { - let mut sources = HashMap::with_capacity(1); - let main = create_source(MAIN, content); - let main_id = main.id(); - sources.insert(main_id, main); - - Self { - library: LazyHash::new(Library::default()), - book, - fonts, - source: sources, - } - } -} - -impl typst::World for WorldPlay<'_> { - /// The standard library. - /// - /// Can be created through `Library::build()`. - fn library(&self) -> &LazyHash { - &self.library - } - - /// Metadata about all known fonts. - fn book(&self) -> &LazyHash { - self.book - } - - /// Get the file id of the main source file. - fn main(&self) -> FileId { - create_file_id(MAIN) - } - - /// Try to access the specified source file. - fn source(&self, id: FileId) -> FileResult { - match self.source.get(&id) { - Some(s) => Ok(s.clone()), - None => FileResult::Err(FileError::NotSource), - } - } - - /// Try to access the specified file. - /// WARNING: This function will only return [FileError] as it is - /// is not implemented, nor needed for Rustwell. - fn file(&self, _: FileId) -> FileResult { - FileResult::Err(FileError::NotSource) - } - - /// Try to access the font with the given index in the font book. - fn font(&self, index: usize) -> Option { - self.fonts.get(index).cloned() - } - - /// Gets the current system time. - /// WARNING: This function will only return [None] as it is - /// is not implemented, nor needed for Rustwell. - fn today(&self, _: Option) -> Option { - None - } -} - -/// Creates a [Source] based on a document. -fn create_source(filename: &str, content: String) -> Source { - let file_id = create_file_id(filename); - Source::new(file_id, content) -} - -/// Creates a [FileId] based on a filename. -fn create_file_id(filename: &str) -> FileId { - FileId::new(None, VirtualPath::new(filename)) -} - -/// Creates a [FontBook] which indexes the returned [Vec]. -fn create_fontbook() -> (LazyHash, Vec) { - let mut fonts = Vec::new(); - let mut fontbook = FontBook::new(); - - for font_data in FONTS.iter() { - let font = match Font::new(Bytes::new(font_data), 0) { - Some(f) => f, - None => continue, - }; - fonts.push(font); - - let info = FontInfo::new(font_data, 0).expect("Could not parse font"); - fontbook.push(info); - } - - (LazyHash::new(fontbook), fonts) -} diff --git a/crates/rustwell/src/lib.rs b/crates/rustwell/src/lib.rs index 009bea9..5dfbae9 100644 --- a/crates/rustwell/src/lib.rs +++ b/crates/rustwell/src/lib.rs @@ -43,8 +43,6 @@ pub use export::Exporter; pub use export::ExporterExt; pub use export::html::HtmlExporter; pub use export::pdf::PdfExporter; -pub use export::pdf2::Pdf2Exporter; -pub use export::typst::TypstExporter; /// Parses a Fountain source string into a [Screenplay] structure. /// From 8e95c167ac3b3bea2231e9b42fcaa61616d0fd5c Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sun, 1 Mar 2026 21:50:54 +0100 Subject: [PATCH 19/39] more name changes! --- crates/rustwell/src/export/pdf.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index df10cf6..34c1bbb 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -203,7 +203,7 @@ impl PdfExporter { ) { let mut screenplay_element_idx = 0; - let mut page_index = 0; + let mut page_idx = 0; let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; let mut residual_breakpoint_idx = None; @@ -230,7 +230,7 @@ impl PdfExporter { } let element = &screenplay.elements[screenplay_element_idx]; - let mut breakpoint_index = match residual_breakpoint_idx { + let mut breakpoint_idx = match residual_breakpoint_idx { Some(i) => { residual_breakpoint_idx = None; i @@ -243,7 +243,7 @@ impl PdfExporter { size, content, margin, - &mut breakpoint_index, + &mut breakpoint_idx, &mut line_idx, max_lines_per_page, &mut surface, @@ -266,9 +266,11 @@ impl PdfExporter { right: 18.0, }; + let rich_number = &number.as_ref().unwrap().into(); + write_element( size, - &number.as_ref().unwrap().into(), + &rich_number, &left_number_margin, &mut 0, &mut initial_line_index.clone(), @@ -280,7 +282,7 @@ impl PdfExporter { write_element( size, - &number.as_ref().unwrap().into(), + &rich_number, &right_number_margin, &mut 0, &mut initial_line_index.clone(), @@ -293,7 +295,7 @@ impl PdfExporter { outline.push_child(OutlineNode::new( slug.to_string(), XyzDestination::new( - page_index, + page_idx, Point { x: MARGINS.heading.left, y: (TOP_MARGIN + (line_idx * FONT_SIZE) - FONT_SIZE) as f32, @@ -304,7 +306,7 @@ impl PdfExporter { size, slug, &MARGINS.heading, - &mut breakpoint_index, + &mut breakpoint_idx, &mut line_idx, max_lines_per_page, &mut surface, @@ -384,7 +386,7 @@ impl PdfExporter { size, s, &MARGINS.synopsis, - &mut breakpoint_index, + &mut breakpoint_idx, &mut line_idx, max_lines_per_page, &mut surface, @@ -406,7 +408,7 @@ impl PdfExporter { surface.finish(); page.finish(); - page_index += 1; + page_idx += 1; } document.set_outline(outline); } From e53626461be680f66830b6f0f8a33347d6814281 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 11 Mar 2026 21:56:18 +0100 Subject: [PATCH 20/39] add dash when breaking word --- crates/rustwell/src/export/pdf.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 34c1bbb..45b21b3 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -589,9 +589,9 @@ fn write_line( None => todo!(), } - let breakpoint_index = match breakpoint { - Some(b) => b.index, - None => content.len(), + let (breakpoint_index, break_word) = match breakpoint { + Some(b) => (b.index, b.break_type == BreakType::BreakWord), + None => (content.len(), false), }; match text_direction { @@ -648,6 +648,17 @@ fn write_line( glyph_index += relative_break_index - relative_index; start_index += relative_break_index - relative_index; } + + if break_word { + surface.draw_text( + Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), + fonts.regular.clone(), + FONT_SIZE as f32, + "-".into(), + false, + krilla::text::TextDirection::LeftToRight, + ); + } } fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { From c51128fa41adca687c3bc1db11b66f77738f597f Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 11 Mar 2026 22:28:43 +0100 Subject: [PATCH 21/39] fix bug with skipping dialogue --- crates/rustwell/src/export/pdf.rs | 59 +++++++++++++++++-------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 45b21b3..442fed2 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -316,7 +316,7 @@ impl PdfExporter { } Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { - write_dialogue( + let premature_exit = write_dialogue( dialogue, &mut residual_dialogue_idx, &mut residual_breakpoint_idx, @@ -327,47 +327,51 @@ impl PdfExporter { fonts, &MARGINS.dialogue, ); - if residual_dialogue_idx.is_some() { + if residual_dialogue_idx.is_some() || premature_exit { break; } } Element::DualDialogue(dialogue0, dialogue1) => { let mut initial_line_index = line_idx; + let mut premature_exit = false; if (residual_dual_dialogue_idx.0.is_none() && residual_dual_dialogue_idx.1.is_none()) || residual_dual_dialogue_idx.0.is_some() { - write_dialogue( - dialogue0, - &mut residual_dual_dialogue_idx.0, - &mut residual_dual_breakpoint_idx.0, - size, - max_lines_per_page, - &mut line_idx, - &mut surface, - fonts, - &MARGINS.dual_dialogue.left, - ); + premature_exit = premature_exit + || write_dialogue( + dialogue0, + &mut residual_dual_dialogue_idx.0, + &mut residual_dual_breakpoint_idx.0, + size, + max_lines_per_page, + &mut line_idx, + &mut surface, + fonts, + &MARGINS.dual_dialogue.left, + ); } if (residual_dual_dialogue_idx.1.is_none() && residual_dual_dialogue_idx.0.is_none()) || residual_dual_dialogue_idx.1.is_some() { - write_dialogue( - dialogue1, - &mut residual_dual_dialogue_idx.1, - &mut residual_dual_breakpoint_idx.1, - size, - max_lines_per_page, - &mut initial_line_index, - &mut surface, - fonts, - &MARGINS.dual_dialogue.right, - ); + premature_exit = premature_exit + || write_dialogue( + dialogue1, + &mut residual_dual_dialogue_idx.1, + &mut residual_dual_breakpoint_idx.1, + size, + max_lines_per_page, + &mut initial_line_index, + &mut surface, + fonts, + &MARGINS.dual_dialogue.right, + ); } line_idx = line_idx.max(initial_line_index); if residual_dual_dialogue_idx.0.is_some() || residual_dual_dialogue_idx.1.is_some() + || premature_exit { break; } @@ -424,7 +428,7 @@ fn write_dialogue( surface: &mut Surface, fonts: &Fonts, dialogue_margins: &DialogueMargins, -) { +) -> bool { let mut character_name = dialogue.character.clone(); match (*residual_dialogue, &dialogue.extension) { (Some(_), _) => { @@ -444,7 +448,7 @@ fn write_dialogue( ); let name_lines_count = break_points(&character_name, span).len() + 1; if *line_index + name_lines_count + 1 >= max_lines { - return; + return true; } assert!(name_lines_count < max_lines); @@ -476,7 +480,7 @@ fn write_dialogue( Alignment::LeftToRight, ); - return; + return false; } let mut breakpoint_index = match *residual_index { Some(i) => { @@ -523,6 +527,7 @@ fn write_dialogue( } *residual_dialogue = None; + false } fn write_element( From 433f65a7a0506d1ef1b68d1321156c9e8b430e7b Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 11 Mar 2026 22:54:17 +0100 Subject: [PATCH 22/39] add page numbers --- crates/rustwell/src/export/pdf.rs | 50 ++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 442fed2..1c35289 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -75,6 +75,7 @@ struct Margins { pub transition: Margin, pub centered: Margin, pub synopsis: Margin, + pub page_number: Margin, } const MARGINS: Margins = Margins { @@ -146,6 +147,10 @@ const MARGINS: Margins = Margins { left: 108.0, right: 72.0, }, + page_number: Margin { + left: 108.0, + right: 72.0, + }, }; /// A [`Screenplay`] exporter for `pdf` @@ -220,6 +225,23 @@ impl PdfExporter { let mut surface = page.surface(); let mut line_idx = 0; + if page_idx > 0 { + let residual_page_number = write_element_custom_top_margin( + size, + &format!("{}.", page_idx + 1).into(), + &MARGINS.page_number, + &mut 0, + &mut 0, + 2, + &mut surface, + fonts, + Alignment::RightToLeft, + 36, + ); + + assert!(residual_page_number.is_none()); + } + loop { if line_idx >= max_lines_per_page { break; @@ -540,6 +562,32 @@ fn write_element( surface: &mut Surface, fonts: &Fonts, text_direction: Alignment, +) -> Option { + write_element_custom_top_margin( + size, + content, + margin, + breakpoint_index, + line_index, + max_lines, + surface, + fonts, + text_direction, + TOP_MARGIN, + ) +} + +fn write_element_custom_top_margin( + size: &PaperSize, + content: &RichString, + margin: &Margin, + breakpoint_index: &mut usize, + line_index: &mut usize, + max_lines: usize, + surface: &mut Surface, + fonts: &Fonts, + text_direction: Alignment, + top_margin: usize, ) -> Option { let left_margin = margin.left; let right_margin = margin.right; @@ -558,7 +606,7 @@ fn write_element( write_line( surface, left_margin, - (FONT_SIZE * *line_index + TOP_MARGIN) as f32, + (FONT_SIZE * *line_index + top_margin) as f32, content, start_index, breakpoints.get(*breakpoint_index), From 69159705ac301f3ff8d7d0c0626289c10c3ffb9a Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 13 Mar 2026 14:27:29 +0100 Subject: [PATCH 23/39] Return results --- crates/rustwell/src/export/pdf.rs | 98 +++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 1c35289..de02d7a 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -182,7 +182,7 @@ impl Exporter for PdfExporter { bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), }; - self.generate_pdf(&mut document, &A4, screenplay, &fonts); + self.generate_pdf(&mut document, &A4, screenplay, &fonts)?; let pdf = document .finish() @@ -205,7 +205,7 @@ impl PdfExporter { size: &PaperSize, screenplay: &Screenplay, fonts: &Fonts, - ) { + ) -> std::io::Result<()> { let mut screenplay_element_idx = 0; let mut page_idx = 0; @@ -225,21 +225,36 @@ impl PdfExporter { let mut surface = page.surface(); let mut line_idx = 0; - if page_idx > 0 { + if (screenplay.titlepage.is_none() && page_idx > 0) + || (screenplay.titlepage.is_some() && page_idx > 1) + { let residual_page_number = write_element_custom_top_margin( size, - &format!("{}.", page_idx + 1).into(), + &format!( + "{}.", + if screenplay.titlepage.is_some() { + page_idx + } else { + page_idx + 1 + } + ) + .into(), &MARGINS.page_number, &mut 0, &mut 0, - 2, + 1, &mut surface, fonts, Alignment::RightToLeft, 36, - ); + )?; - assert!(residual_page_number.is_none()); + if residual_page_number.is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "There cannot be more pages than the number which fits on a page.", + )); + } } loop { @@ -272,6 +287,7 @@ impl PdfExporter { fonts, text_direction, ) + .unwrap() }; match &element { @@ -300,7 +316,7 @@ impl PdfExporter { &mut surface, fonts, Alignment::LeftToRight, - ); + )?; write_element( size, @@ -312,7 +328,7 @@ impl PdfExporter { &mut surface, fonts, Alignment::RightToLeft, - ); + )?; } outline.push_child(OutlineNode::new( slug.to_string(), @@ -334,7 +350,7 @@ impl PdfExporter { &mut surface, fonts, Alignment::LeftToRight, - ) + )? } Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { @@ -348,7 +364,7 @@ impl PdfExporter { &mut surface, fonts, &MARGINS.dialogue, - ); + )?; if residual_dialogue_idx.is_some() || premature_exit { break; } @@ -371,7 +387,7 @@ impl PdfExporter { &mut surface, fonts, &MARGINS.dual_dialogue.left, - ); + )?; } if (residual_dual_dialogue_idx.1.is_none() && residual_dual_dialogue_idx.0.is_none()) @@ -388,7 +404,7 @@ impl PdfExporter { &mut surface, fonts, &MARGINS.dual_dialogue.right, - ); + )?; } line_idx = line_idx.max(initial_line_index); if residual_dual_dialogue_idx.0.is_some() @@ -418,7 +434,7 @@ impl PdfExporter { &mut surface, fonts, Alignment::LeftToRight, - ); + )?; surface.set_fill(None); } } @@ -437,6 +453,8 @@ impl PdfExporter { page_idx += 1; } document.set_outline(outline); + + Ok(()) } } @@ -450,7 +468,7 @@ fn write_dialogue( surface: &mut Surface, fonts: &Fonts, dialogue_margins: &DialogueMargins, -) -> bool { +) -> std::io::Result { let mut character_name = dialogue.character.clone(); match (*residual_dialogue, &dialogue.extension) { (Some(_), _) => { @@ -470,9 +488,15 @@ fn write_dialogue( ); let name_lines_count = break_points(&character_name, span).len() + 1; if *line_index + name_lines_count + 1 >= max_lines { - return true; + return Ok(true); + } + + if name_lines_count >= max_lines { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Character name cannot be longer than a whole page.", + )); } - assert!(name_lines_count < max_lines); write_element( size, @@ -484,7 +508,7 @@ fn write_dialogue( surface, fonts, Alignment::LeftToRight, - ); + )?; let mut dialogue_index = residual_dialogue.unwrap_or(0); while dialogue_index < dialogue.elements.len() { @@ -500,9 +524,9 @@ fn write_dialogue( surface, fonts, Alignment::LeftToRight, - ); + )?; - return false; + return Ok(false); } let mut breakpoint_index = match *residual_index { Some(i) => { @@ -524,7 +548,7 @@ fn write_dialogue( surface, fonts, Alignment::LeftToRight, - ) + )? } DialogueElement::Line(s) => { *residual_index = write_element( @@ -537,7 +561,7 @@ fn write_dialogue( surface, fonts, Alignment::LeftToRight, - ) + )? } } @@ -549,7 +573,7 @@ fn write_dialogue( } *residual_dialogue = None; - false + Ok(false) } fn write_element( @@ -562,7 +586,7 @@ fn write_element( surface: &mut Surface, fonts: &Fonts, text_direction: Alignment, -) -> Option { +) -> std::io::Result> { write_element_custom_top_margin( size, content, @@ -588,14 +612,14 @@ fn write_element_custom_top_margin( fonts: &Fonts, text_direction: Alignment, top_margin: usize, -) -> Option { +) -> std::io::Result> { let left_margin = margin.left; let right_margin = margin.right; let span = glyph_span(size, left_margin, right_margin); let breakpoints = break_points(content, span); while *breakpoint_index <= breakpoints.len() { if *line_index >= max_lines { - return Some(*breakpoint_index); + return Ok(Some(*breakpoint_index)); } let start_index = if *breakpoint_index == 0 { @@ -614,11 +638,11 @@ fn write_element_custom_top_margin( text_direction, size, margin, - ); + )?; *breakpoint_index += 1; *line_index += 1; } - None + Ok(None) } fn write_line( @@ -632,14 +656,19 @@ fn write_line( text_direction: Alignment, size: &PaperSize, margin: &Margin, -) { +) -> std::io::Result<()> { match content.get_char(start_index) { Some(c) => { if c == '\n' { start_index += 1 } } - None => todo!(), + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Could not get character from source.", + )); + } } let (breakpoint_index, break_word) = match breakpoint { @@ -665,7 +694,12 @@ fn write_line( while start_index < breakpoint_index { let (string_element, relative_index) = match content.get_element_from_index(start_index) { Some(res) => res, - None => todo!(), + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Could not get rich string element.", + )); + } }; let element_length = string_element.text.chars().count(); @@ -712,6 +746,8 @@ fn write_line( krilla::text::TextDirection::LeftToRight, ); } + + Ok(()) } fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { From 0a05c0ec9a357d32daa7d93ffccd38b0dc60622e Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 13 Mar 2026 15:25:26 +0100 Subject: [PATCH 24/39] Replace closure with macro --- crates/rustwell/src/export/pdf.rs | 69 +++++++++++++------------------ 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index de02d7a..f192754 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -275,20 +275,21 @@ impl PdfExporter { None => 0, }; - let mut we = |content, margin, text_direction| { - residual_breakpoint_idx = write_element( - size, - content, - margin, - &mut breakpoint_idx, - &mut line_idx, - max_lines_per_page, - &mut surface, - fonts, - text_direction, - ) - .unwrap() - }; + macro_rules! write_element { + ($content:expr, $margin:expr, $text_direction:expr) => { + residual_breakpoint_idx = write_element( + size, + $content, + $margin, + &mut breakpoint_idx, + &mut line_idx, + max_lines_per_page, + &mut surface, + fonts, + $text_direction, + )? + }; + } match &element { Element::Heading { slug, number } => { @@ -340,19 +341,11 @@ impl PdfExporter { }, ), )); - residual_breakpoint_idx = write_element( - size, - slug, - &MARGINS.heading, - &mut breakpoint_idx, - &mut line_idx, - max_lines_per_page, - &mut surface, - fonts, - Alignment::LeftToRight, - )? + write_element!(slug, &MARGINS.heading, Alignment::LeftToRight); + } + Element::Action(s) => { + write_element!(s, &MARGINS.action, Alignment::LeftToRight); } - Element::Action(s) => we(s, &MARGINS.action, Alignment::LeftToRight), Element::Dialogue(dialogue) => { let premature_exit = write_dialogue( dialogue, @@ -414,9 +407,15 @@ impl PdfExporter { break; } } - Element::Lyrics(s) => we(s, &MARGINS.lyrics, Alignment::RightToLeft), - Element::Transition(s) => we(s, &MARGINS.transition, Alignment::RightToLeft), - Element::CenteredText(s) => we(s, &MARGINS.centered, Alignment::Centered), + Element::Lyrics(s) => { + write_element!(s, &MARGINS.lyrics, Alignment::RightToLeft); + } + Element::Transition(s) => { + write_element!(s, &MARGINS.transition, Alignment::RightToLeft); + } + Element::CenteredText(s) => { + write_element!(s, &MARGINS.centered, Alignment::Centered); + } Element::Synopsis(s) => { if self.synopses { surface.set_fill(Some(Fill { @@ -424,17 +423,7 @@ impl PdfExporter { opacity: NormalizedF32::new(0.5).unwrap(), rule: Default::default(), })); - write_element( - size, - s, - &MARGINS.synopsis, - &mut breakpoint_idx, - &mut line_idx, - max_lines_per_page, - &mut surface, - fonts, - Alignment::LeftToRight, - )?; + write_element!(s, &MARGINS.synopsis, Alignment::LeftToRight); surface.set_fill(None); } } From 18214355b4b1e6f951ff8c0dbcda12f2f60e606d Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 13 Mar 2026 16:29:14 +0100 Subject: [PATCH 25/39] Fix export bugs --- crates/rustwell/src/export/pdf.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index f192754..d17271f 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -269,7 +269,9 @@ impl PdfExporter { let element = &screenplay.elements[screenplay_element_idx]; let mut breakpoint_idx = match residual_breakpoint_idx { Some(i) => { - residual_breakpoint_idx = None; + if !matches!(element, Element::Dialogue(_)) { + residual_breakpoint_idx = None; + } i } None => 0, @@ -434,6 +436,11 @@ impl PdfExporter { } line_idx += 1; + + if residual_breakpoint_idx.is_some() { + continue; + } + screenplay_element_idx += 1; } @@ -476,9 +483,6 @@ fn write_dialogue( dialogue_margins.character.right, ); let name_lines_count = break_points(&character_name, span).len() + 1; - if *line_index + name_lines_count + 1 >= max_lines { - return Ok(true); - } if name_lines_count >= max_lines { return Err(std::io::Error::new( @@ -487,6 +491,10 @@ fn write_dialogue( )); } + if *line_index + name_lines_count + 1 >= max_lines { + return Ok(true); + } + write_element( size, &character_name, @@ -501,7 +509,7 @@ fn write_dialogue( let mut dialogue_index = residual_dialogue.unwrap_or(0); while dialogue_index < dialogue.elements.len() { - if *line_index + 1 >= max_lines { + if *line_index >= max_lines { *residual_dialogue = Some(dialogue_index); write_element( size, @@ -515,7 +523,7 @@ fn write_dialogue( Alignment::LeftToRight, )?; - return Ok(false); + return Ok(true); } let mut breakpoint_index = match *residual_index { Some(i) => { From b856391d58ac22d3157a20d945b840f57379b129 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 13 Mar 2026 22:43:35 +0100 Subject: [PATCH 26/39] add underline --- crates/rustwell/src/export/pdf.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index d17271f..9e96da4 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -4,7 +4,7 @@ use krilla::{ Document, color::rgb, destination::XyzDestination, - geom::Point, + geom::{PathBuilder, Point, Rect}, num::NormalizedF32, outline::{Outline, OutlineNode}, page::PageSettings, @@ -729,8 +729,27 @@ fn write_line( krilla::text::TextDirection::LeftToRight, ); - glyph_index += relative_break_index - relative_index; - start_index += relative_break_index - relative_index; + let glyphs_written = relative_break_index - relative_index; + + if string_element.is_underline() { + let underline = { + let mut pb = PathBuilder::new(); + let r = Rect::from_xywh( + x + (glyph_index as f32 * FONT_WIDTH), + y + 0.5, + glyphs_written as f32 * FONT_WIDTH, + 0.75, + ) + .unwrap(); + pb.push_rect(r); + pb.close(); + pb.finish().unwrap() + }; + surface.draw_path(&underline); + } + + glyph_index += glyphs_written; + start_index += glyphs_written; } if break_word { From faaf14e5f78eadb470532fda5e00ac8cd8dc9651 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Fri, 13 Mar 2026 23:50:45 +0100 Subject: [PATCH 27/39] Clean up a bit and start documentation process --- crates/rustwell/src/export/pdf.rs | 47 ++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 9e96da4..3b2d0b7 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -19,7 +19,7 @@ use crate::{ screenplay::{Dialogue, DialogueElement, Element}, }; -const FONT_SIZE: usize = 12; +const FONT_SIZE: usize = 12; // standard screenplay size const FONT_WIDTH: f32 = 7.2; // 12 * 0.6 (Courier Prime's aspect ratio) /// The font bundled together with Rustwell; Courier Prime. @@ -32,40 +32,56 @@ const FONTS: [&[u8]; 4] = [ include_bytes!("fonts/CourierPrime-BoldItalic.ttf"), ]; -struct Fonts { +/// A family of fonts with the standard variants. +struct FontFamily { pub regular: Font, pub bold: Font, pub italic: Font, pub bold_italic: Font, } +/// Dimensions of a paper in points (pts). pub struct PaperSize { pub x: usize, pub y: usize, } +/// The size of an `A4` paper in points (pts). pub const A4: PaperSize = PaperSize { x: 595, y: 842 }; // A4 size in pts +/// The size of a `US letter` paper in points (pts). pub const LETTER: PaperSize = PaperSize { x: 612, y: 792 }; // Letter size in pts +impl Default for PaperSize { + fn default() -> Self { + A4 + } +} + +/// The margin at the top of a page. Applicable on every page. In points. const TOP_MARGIN: usize = 72; +/// The margin at the bottom of a page. Applicable on every page. In points. const BOTTOM_MARGIN: usize = 72; +/// Left- and right margins, in points. struct Margin { pub left: f32, pub right: f32, } +/// Collection of margins for the dialogue components. struct DialogueMargins { pub character: Margin, pub parenthetical: Margin, pub line: Margin, } +/// Collection of margins for the dual dialogue components. struct DualDialogueMargins { pub left: DialogueMargins, pub right: DialogueMargins, } +/// Collection of all margins for all different screenplay [`Elements`]. struct Margins { pub heading: Margin, pub action: Margin, @@ -78,6 +94,7 @@ struct Margins { pub page_number: Margin, } +/// The standard margins for all different screenplay [`Elements`]. const MARGINS: Margins = Margins { heading: Margin { left: 108.0, @@ -160,14 +177,18 @@ const MARGINS: Margins = Margins { pub struct PdfExporter { /// Whether to include synopses in the output pub synopses: bool, + /// What size (type) of paper (e.g. A4 or US letter) + pub paper_size: PaperSize, } impl Exporter for PdfExporter { + /// The `.pdf` extension. fn file_extension(&self) -> &'static str { "pdf" } - /// Exports a `pdf` file and writes it to the provided writer. + /// Exports a `pdf` file and writes it to the provided writer. The pdf creation can fail if + /// certain elements do not fit within a single page. fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> { let regular_data: Arc + Send + Sync> = Arc::new(FONTS[0]); let bold_data: Arc + Send + Sync> = Arc::new(FONTS[1]); @@ -175,14 +196,14 @@ impl Exporter for PdfExporter { let bold_italic_data: Arc + Send + Sync> = Arc::new(FONTS[3]); let mut document = Document::new(); - let fonts = Fonts { + let fonts = FontFamily { regular: Font::new(regular_data.into(), 0).unwrap(), bold: Font::new(bold_data.into(), 0).unwrap(), italic: Font::new(italic_data.into(), 0).unwrap(), bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), }; - self.generate_pdf(&mut document, &A4, screenplay, &fonts)?; + self.generate_pdf(&mut document, &self.paper_size, screenplay, &fonts)?; let pdf = document .finish() @@ -199,17 +220,22 @@ enum Alignment { } impl PdfExporter { + /// Generates a `pdf` document from a [`Screenplay`]. Runs in (more or less) a single pass. fn generate_pdf( &self, document: &mut Document, size: &PaperSize, screenplay: &Screenplay, - fonts: &Fonts, + fonts: &FontFamily, ) -> std::io::Result<()> { + // The index for which element in the screenplay is currently being processed. let mut screenplay_element_idx = 0; + // The index for which page in the document is currently being written. let mut page_idx = 0; + // The maximum number of writable lines which can fit on a page, considering the top and + // bottom margins. let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; let mut residual_breakpoint_idx = None; let mut residual_dialogue_idx = None; @@ -225,6 +251,7 @@ impl PdfExporter { let mut surface = page.surface(); let mut line_idx = 0; + // Writes the page number. if (screenplay.titlepage.is_none() && page_idx > 0) || (screenplay.titlepage.is_some() && page_idx > 1) { @@ -462,7 +489,7 @@ fn write_dialogue( max_lines: usize, line_index: &mut usize, surface: &mut Surface, - fonts: &Fonts, + fonts: &FontFamily, dialogue_margins: &DialogueMargins, ) -> std::io::Result { let mut character_name = dialogue.character.clone(); @@ -581,7 +608,7 @@ fn write_element( line_index: &mut usize, max_lines: usize, surface: &mut Surface, - fonts: &Fonts, + fonts: &FontFamily, text_direction: Alignment, ) -> std::io::Result> { write_element_custom_top_margin( @@ -606,7 +633,7 @@ fn write_element_custom_top_margin( line_index: &mut usize, max_lines: usize, surface: &mut Surface, - fonts: &Fonts, + fonts: &FontFamily, text_direction: Alignment, top_margin: usize, ) -> std::io::Result> { @@ -649,7 +676,7 @@ fn write_line( content: &RichString, mut start_index: usize, breakpoint: Option<&BreakPoint>, - fonts: &Fonts, + fonts: &FontFamily, text_direction: Alignment, size: &PaperSize, margin: &Margin, From 40fbc060fdbb848228e252db65ae61c556ea2351 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 14 Mar 2026 15:49:44 +0100 Subject: [PATCH 28/39] Add titlepage generator --- crates/rustwell/src/export/pdf.rs | 141 +++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 3b2d0b7..f7c3e5a 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -16,7 +16,7 @@ use krilla::{ use crate::{ Exporter, Screenplay, rich_string::RichString, - screenplay::{Dialogue, DialogueElement, Element}, + screenplay::{Dialogue, DialogueElement, Element, TitlePage}, }; const FONT_SIZE: usize = 12; // standard screenplay size @@ -245,6 +245,11 @@ impl PdfExporter { let mut outline = Outline::new(); + if let Some(t) = &screenplay.titlepage { + page_idx += 1; + write_titlepage(size, t, max_lines_per_page, document, fonts)?; + } + while screenplay_element_idx < screenplay.elements.len() { let mut page = document .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); @@ -793,6 +798,140 @@ fn write_line( Ok(()) } +// pub title: Vec, +// pub credit: Vec, +// pub authors: Vec, +// pub source: Vec, +// pub draft_date: Vec, +// pub contact: Vec, +struct TitlePageMargins { + pub title: Margin, + pub credit: Margin, + pub authors: Margin, + pub source: Margin, + pub draft_date: Margin, + pub contact: Margin, +} + +const TITLE_PAGE_MARGINS: TitlePageMargins = TitlePageMargins { + title: Margin { + left: 72.0, + right: 72.0, + }, + credit: Margin { + left: 72.0, + right: 72.0, + }, + authors: Margin { + left: 72.0, + right: 72.0, + }, + source: Margin { + left: 72.0, + right: 72.0, + }, + draft_date: Margin { + left: 315.0, + right: 72.0, + }, + contact: Margin { + left: 72.0, + right: 315.0, + }, +}; + +fn write_titlepage( + size: &PaperSize, + titlepage: &TitlePage, + max_lines: usize, + document: &mut Document, + fonts: &FontFamily, +) -> std::io::Result<()> { + let mut page = + document.start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); + let mut surface = page.surface(); + + let mut line_idx = max_lines / 3; + + macro_rules! write_title_element { + ($element:ident) => { + if !titlepage.$element.is_empty() { + for s in &titlepage.$element { + let residual = write_element( + size, + s, + &TITLE_PAGE_MARGINS.$element, + &mut 0, + &mut line_idx, + max_lines, + &mut surface, + fonts, + Alignment::Centered, + )?; + + if residual.is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Title page cannot be longer than a single page.", + )); + } + } + line_idx += 1; + } + }; + ($element:ident, $alignment:expr) => { + if !titlepage.$element.is_empty() { + let mut total_lines = titlepage.$element.len(); + for s in &titlepage.$element { + total_lines += break_points( + s, + glyph_span( + size, + TITLE_PAGE_MARGINS.$element.left, + TITLE_PAGE_MARGINS.$element.right, + ), + ) + .len(); + + if total_lines > max_lines { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Title page cannot be longer than a single page.", + )); + } + } + line_idx = max_lines - total_lines; + + for s in &titlepage.$element { + write_element( + size, + s, + &TITLE_PAGE_MARGINS.$element, + &mut 0, + &mut line_idx, + max_lines, + &mut surface, + fonts, + $alignment, + )?; + } + } + }; + } + + write_title_element!(title); + write_title_element!(credit); + write_title_element!(authors); + write_title_element!(source); + + write_title_element!(contact, Alignment::LeftToRight); + write_title_element!(draft_date, Alignment::RightToLeft); + + surface.finish(); + page.finish(); + Ok(()) +} + fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { ((size.x as f32 - (left_margin + right_margin)) / FONT_WIDTH) as usize } From ea24432c998d2fd878b6e149b306832680deb348 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 14 Mar 2026 15:52:58 +0100 Subject: [PATCH 29/39] Some clippy fixes --- crates/rustwell/src/export/pdf.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index f7c3e5a..ffa2a30 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -343,7 +343,7 @@ impl PdfExporter { write_element( size, - &rich_number, + rich_number, &left_number_margin, &mut 0, &mut initial_line_index.clone(), @@ -355,7 +355,7 @@ impl PdfExporter { write_element( size, - &rich_number, + rich_number, &right_number_margin, &mut 0, &mut initial_line_index.clone(), @@ -569,7 +569,7 @@ fn write_dialogue( DialogueElement::Parenthetical(s) => { *residual_index = write_element( size, - &s, + s, &dialogue_margins.parenthetical, &mut breakpoint_index, line_index, @@ -582,7 +582,7 @@ fn write_dialogue( DialogueElement::Line(s) => { *residual_index = write_element( size, - &s, + s, &dialogue_margins.line, &mut breakpoint_index, line_index, @@ -789,7 +789,7 @@ fn write_line( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), fonts.regular.clone(), FONT_SIZE as f32, - "-".into(), + "-", false, krilla::text::TextDirection::LeftToRight, ); From 83fca957d7a0a6cb5b2f50f433d52377c99feadf Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 14 Mar 2026 18:06:28 +0100 Subject: [PATCH 30/39] Add more documentation --- crates/rustwell/src/export/pdf.rs | 107 +++++++++++++++++++---------- crates/rustwell/src/rich_string.rs | 30 +++++--- 2 files changed, 92 insertions(+), 45 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index ffa2a30..d674e36 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -62,7 +62,8 @@ const TOP_MARGIN: usize = 72; /// The margin at the bottom of a page. Applicable on every page. In points. const BOTTOM_MARGIN: usize = 72; -/// Left- and right margins, in points. +/// Left- and right margins, in points, going inwards - meaning left margin is relative to the left +/// side, and the right margin is relative to the right side. struct Margin { pub left: f32, pub right: f32, @@ -237,6 +238,8 @@ impl PdfExporter { // The maximum number of writable lines which can fit on a page, considering the top and // bottom margins. let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; + // If an element does not fit within a page, this will be Some(index) pointing to the which + // breakpoint in the breakpoint list to start at on the next page. let mut residual_breakpoint_idx = None; let mut residual_dialogue_idx = None; @@ -250,6 +253,7 @@ impl PdfExporter { write_titlepage(size, t, max_lines_per_page, document, fonts)?; } + // Page loop, creates a new page and writes everything it can on it. while screenplay_element_idx < screenplay.elements.len() { let mut page = document .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); @@ -281,6 +285,7 @@ impl PdfExporter { 36, )?; + // Page number cannot be larger than what fits on a single line on the page. if residual_page_number.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, @@ -289,6 +294,7 @@ impl PdfExporter { } } + // Element loop, iterates through the screenplay elements. loop { if line_idx >= max_lines_per_page { break; @@ -301,6 +307,8 @@ impl PdfExporter { let element = &screenplay.elements[screenplay_element_idx]; let mut breakpoint_idx = match residual_breakpoint_idx { Some(i) => { + // If we're in a dialogue element, we need to preserve + // `residual_breakpoint_idx`. if !matches!(element, Element::Dialogue(_)) { residual_breakpoint_idx = None; } @@ -309,6 +317,8 @@ impl PdfExporter { None => 0, }; + /// Macro for the most common usage of write_element(...), as most types of + /// [`Element`]s call this function with almost identical parameters. macro_rules! write_element { ($content:expr, $margin:expr, $text_direction:expr) => { residual_breakpoint_idx = write_element( @@ -399,6 +409,8 @@ impl PdfExporter { Element::DualDialogue(dialogue0, dialogue1) => { let mut initial_line_index = line_idx; let mut premature_exit = false; + // Ensures dual dialogue is only written it either both have not been + // written, or if this specifically is to be continued on a new page. if (residual_dual_dialogue_idx.0.is_none() && residual_dual_dialogue_idx.1.is_none()) || residual_dual_dialogue_idx.0.is_some() @@ -486,6 +498,9 @@ impl PdfExporter { } } +/// Writes a diologue [`Element`] to the `pdf` document. If a dialogue spans multiple pages it will +/// write the character name with the extension `(cont'd)` on each new page. Returns a +/// [Option] which is true if the whole dialogue element did not fit on the same page. fn write_dialogue( dialogue: &Dialogue, residual_dialogue: &mut Option, @@ -543,6 +558,8 @@ fn write_dialogue( while dialogue_index < dialogue.elements.len() { if *line_index >= max_lines { *residual_dialogue = Some(dialogue_index); + // If a dialogue continues on the next page, writes `(MORE)` at the bottom of the + // current page, inside the bottom margin. write_element( size, &"(MORE)".into(), @@ -565,34 +582,22 @@ fn write_dialogue( None => 0, }; - match &dialogue.elements[dialogue_index] { - DialogueElement::Parenthetical(s) => { - *residual_index = write_element( - size, - s, - &dialogue_margins.parenthetical, - &mut breakpoint_index, - line_index, - max_lines, - surface, - fonts, - Alignment::LeftToRight, - )? - } - DialogueElement::Line(s) => { - *residual_index = write_element( - size, - s, - &dialogue_margins.line, - &mut breakpoint_index, - line_index, - max_lines, - surface, - fonts, - Alignment::LeftToRight, - )? - } - } + let (content, margin) = match &dialogue.elements[dialogue_index] { + DialogueElement::Parenthetical(s) => (s, &dialogue_margins.parenthetical), + DialogueElement::Line(s) => (s, &dialogue_margins.line), + }; + + *residual_index = write_element( + size, + content, + margin, + &mut breakpoint_index, + line_index, + max_lines, + surface, + fonts, + Alignment::LeftToRight, + )?; if residual_index.is_some() { continue; @@ -605,6 +610,9 @@ fn write_dialogue( Ok(false) } +/// Writes a [`Element`] to the `pdf` document. If it cannot fit the whole element on the page it +/// will return [`Some`] with an index to which [`BreakPoint`] the function made it to. Calls +/// [`write_element_custom_top_margin`] with the standard top margin. fn write_element( size: &PaperSize, content: &RichString, @@ -630,6 +638,9 @@ fn write_element( ) } +/// Writes a [`Element`] to the `pdf` document. If it cannot fit the whole element on the page it +/// will return [`Some`] with an index to which [`BreakPoint`] the function made it to. Allows for +/// using a non-default top margin (for page numbers). fn write_element_custom_top_margin( size: &PaperSize, content: &RichString, @@ -674,12 +685,13 @@ fn write_element_custom_top_margin( Ok(None) } +/// Writes a single line (not to be confused with [`DialogueElement::Line`]) onto the page. fn write_line( surface: &mut Surface, mut x: f32, y: f32, content: &RichString, - mut start_index: usize, + mut start_index: usize, // the "global" index to the rich string breakpoint: Option<&BreakPoint>, fonts: &FontFamily, text_direction: Alignment, @@ -687,6 +699,8 @@ fn write_line( margin: &Margin, ) -> std::io::Result<()> { match content.get_char(start_index) { + // Newlines are handled as part of the layout engine, so it should never write ut a newline + // character. Some(c) => { if c == '\n' { start_index += 1 @@ -705,6 +719,8 @@ fn write_line( None => (content.len(), false), }; + // If we use a nonstandard [`Alignment`] we must move the `x` value of which we start writing + // the line on. match text_direction { Alignment::LeftToRight => (), Alignment::RightToLeft => { @@ -719,7 +735,9 @@ fn write_line( } } + // Keeps track of the position on the line which the writer is currently at. let mut glyph_index = 0; + // Writes until reaching the breakpoint position. while start_index < breakpoint_index { let (string_element, relative_index) = match content.get_element_from_index(start_index) { Some(res) => res, @@ -733,6 +751,7 @@ fn write_line( let element_length = string_element.text.chars().count(); + // The local index in the rich string element at which to break the line. let relative_break_index = if breakpoint_index - start_index >= element_length - relative_index { element_length @@ -763,6 +782,7 @@ fn write_line( let glyphs_written = relative_break_index - relative_index; + // Draws a line under the characters (to create an underline effect). if string_element.is_underline() { let underline = { let mut pb = PathBuilder::new(); @@ -784,6 +804,7 @@ fn write_line( start_index += glyphs_written; } + // Adds a `-` at the end of a line if the linebreak took place inside a word. if break_word { surface.draw_text( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), @@ -798,12 +819,7 @@ fn write_line( Ok(()) } -// pub title: Vec, -// pub credit: Vec, -// pub authors: Vec, -// pub source: Vec, -// pub draft_date: Vec, -// pub contact: Vec, +/// Margins for the [`TitlePage`] elements. struct TitlePageMargins { pub title: Margin, pub credit: Margin, @@ -813,6 +829,7 @@ struct TitlePageMargins { pub contact: Margin, } +/// Standard margins for the [`TitlePage`] elements. const TITLE_PAGE_MARGINS: TitlePageMargins = TitlePageMargins { title: Margin { left: 72.0, @@ -840,6 +857,8 @@ const TITLE_PAGE_MARGINS: TitlePageMargins = TitlePageMargins { }, }; +/// Writes the [`TitlePage`] to the `pdf` document. Fails if everything does not fit on one page, +/// but allows for overlapping contact information and draft dates with the rest of the elements. fn write_titlepage( size: &PaperSize, titlepage: &TitlePage, @@ -853,7 +872,10 @@ fn write_titlepage( let mut line_idx = max_lines / 3; + /// Writes the [`TitlePage`] elements using the [`write_element`] function. Will add newlines + /// between each type of element, as per (some) standards. macro_rules! write_title_element { + // For the elements which are centered an written below each other. ($element:ident) => { if !titlepage.$element.is_empty() { for s in &titlepage.$element { @@ -879,6 +901,7 @@ fn write_titlepage( line_idx += 1; } }; + // For elements which have specific alignments and are written from the bottom up. ($element:ident, $alignment:expr) => { if !titlepage.$element.is_empty() { let mut total_lines = titlepage.$element.len(); @@ -932,22 +955,34 @@ fn write_titlepage( Ok(()) } +/// Calculates the span in a margin. Returns how many characters with the standard font width will +/// fit between the left and right margin. fn glyph_span(size: &PaperSize, left_margin: f32, right_margin: f32) -> usize { ((size.x as f32 - (left_margin + right_margin)) / FONT_WIDTH) as usize } +/// The different ways to break a line into multiple lines. #[derive(Debug, PartialEq, Eq, Clone, Hash)] enum BreakType { + /// Standard type of line break, by just adding a newline somewhere. NewLine, + /// Break the line by adding a newline inside a word. BreakWord, } +/// Where in the string a line break should occur. #[derive(Debug, PartialEq, Eq, Clone, Hash)] struct BreakPoint { pub index: usize, + /// If it should add a newline after a word, or inside a word. pub break_type: BreakType, } +/// Given a [`RichString`] and a number of allowed glyphs on a single line, calculates where +/// eventual [`BreakPoint`]s should be placed in the string. +/// +/// Greedily places [`BreakPoint`]s. Will only place one inside a word if the word does not fit on +/// a line by itself. Respects newline characters in the string. fn break_points(content: &RichString, span: usize) -> Vec { debug_assert!(span >= 2); diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index d4a7631..3da7ed0 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -14,7 +14,7 @@ //! assert!(rs.elements[1].is_bold()); //! ``` -use std::str::Chars; +use std::{fmt::Display, str::Chars}; use bitflags::bitflags; @@ -66,6 +66,8 @@ impl RichString { len } + /// Gets a [`char`] from a "global" index, meaning the index when viewing the [`RichString`] as + /// a single string without any style attributes taken into account. pub fn get_char(&self, mut index: usize) -> Option { if index >= self.len() { return None; @@ -80,6 +82,8 @@ impl RichString { None } + /// Given a "global" index, gets the [`Element`] which contains it, and the "local" index + /// pointing to that character in the element. pub fn get_element_from_index(&self, mut index: usize) -> Option<(&Element, usize)> { if index >= self.len() { return None; @@ -94,6 +98,8 @@ impl RichString { None } + /// Creates an [char] iterator over the [`RichString`], without the style attributes of each + /// [char] taken into account. pub fn iter(&'_ self) -> RichIterator<'_> { RichIterator { rich_string: self, @@ -102,6 +108,8 @@ impl RichString { } } + /// Appends a [`RichString`] to self. Will not merge the last [`Element`] of self, and the + /// first of the other even if they have the same style attributes. pub fn append(&mut self, mut other: Self) { self.elements.append(&mut other.elements); } @@ -186,20 +194,22 @@ impl RichString { self.elements.push(Element { text, attributes }); } +} + +impl Default for RichString { + fn default() -> Self { + Self::new() + } +} - pub fn to_string(&self) -> String { +impl Display for RichString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut str = String::with_capacity(self.len()); for element in &self.elements { str.push_str(&element.text); } - str - } -} - -impl Default for RichString { - fn default() -> Self { - Self::new() + write!(f, "{str}") } } @@ -214,6 +224,8 @@ where } } +/// An intermediate iterator which allows for seamless iteration over the [Chars] inside a +/// [`RichString`]. pub struct RichIterator<'a> { rich_string: &'a RichString, element_idx: usize, From 4494c09536170a5be04fc67b3907bec3689f81ed Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Sat, 14 Mar 2026 18:27:36 +0100 Subject: [PATCH 31/39] Add paper size choice to cli --- crates/rustwell-cli/src/main.rs | 19 +++++++++++++++++++ crates/rustwell/src/lib.rs | 3 +++ 2 files changed, 22 insertions(+) diff --git a/crates/rustwell-cli/src/main.rs b/crates/rustwell-cli/src/main.rs index 019e82d..9b47d9c 100644 --- a/crates/rustwell-cli/src/main.rs +++ b/crates/rustwell-cli/src/main.rs @@ -27,6 +27,10 @@ struct Cli { #[arg(short = 't', long = "target", value_enum)] target: Option, + /// Paper size specific for pdf output + #[arg(short = 'p', long = "papersize", value_enum)] + papersize: Option, + /// Alias for stdout (same as `-o -`) #[arg(long = "stdout")] stdout: bool, @@ -42,6 +46,12 @@ enum Target { Pdf, } +#[derive(Debug, Clone, Copy, ValueEnum)] +enum PaperSize { + A4, + Letter, +} + fn main() -> Result<()> { color_eyre::install()?; let cli = Cli::parse(); @@ -74,6 +84,7 @@ fn decide_exporter(cli: &Cli) -> Box { }), Target::Pdf => Box::new(PdfExporter { synopses: cli.synopses, + paper_size: decide_paper_size(cli.papersize), ..Default::default() }), } @@ -135,3 +146,11 @@ fn detect_name_from_path(path: &str) -> Result<&str> { .and_then(|s| s.to_str()) .unwrap_or_default()) } + +fn decide_paper_size(size: Option) -> rustwell::PaperSize { + match size { + Some(PaperSize::A4) => rustwell::A4, + Some(PaperSize::Letter) => rustwell::LETTER, + None => rustwell::PaperSize::default(), + } +} diff --git a/crates/rustwell/src/lib.rs b/crates/rustwell/src/lib.rs index 5dfbae9..830cfb7 100644 --- a/crates/rustwell/src/lib.rs +++ b/crates/rustwell/src/lib.rs @@ -42,6 +42,9 @@ pub use screenplay::Screenplay; pub use export::Exporter; pub use export::ExporterExt; pub use export::html::HtmlExporter; +pub use export::pdf::A4; +pub use export::pdf::LETTER; +pub use export::pdf::PaperSize; pub use export::pdf::PdfExporter; /// Parses a Fountain source string into a [Screenplay] structure. From 6efaefb614f82c80cd9b88e2cb9b664fceee6966 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Tue, 31 Mar 2026 22:34:06 +0200 Subject: [PATCH 32/39] Move to krilla 0.7.0 --- Cargo.lock | 4 ++-- crates/rustwell/Cargo.toml | 2 +- crates/rustwell/src/export/pdf.rs | 14 +++++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e2196f..ec71570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,9 +390,9 @@ checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "krilla" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ddfec86fec13d068075e14f22a7e217c281f3ed69ddcb427bf3f5d504fd674" +checksum = "6ddd739cf930f99a70ee89ec51b21fab894b5f142dc9672e0e9a27947b2bc093" dependencies = [ "base64", "bumpalo", diff --git a/crates/rustwell/Cargo.toml b/crates/rustwell/Cargo.toml index 9ab0061..d6c44b5 100644 --- a/crates/rustwell/Cargo.toml +++ b/crates/rustwell/Cargo.toml @@ -13,4 +13,4 @@ path = "src/lib.rs" [dependencies] bitflags = "2" -krilla = "0.6.0" +krilla = "0.7.0" diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index d674e36..846c80b 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -1,4 +1,4 @@ -use std::{io::Write, sync::Arc}; +use std::io::Write; use krilla::{ Document, @@ -191,17 +191,13 @@ impl Exporter for PdfExporter { /// Exports a `pdf` file and writes it to the provided writer. The pdf creation can fail if /// certain elements do not fit within a single page. fn export(&self, screenplay: &Screenplay, writer: &mut dyn Write) -> std::io::Result<()> { - let regular_data: Arc + Send + Sync> = Arc::new(FONTS[0]); - let bold_data: Arc + Send + Sync> = Arc::new(FONTS[1]); - let italic_data: Arc + Send + Sync> = Arc::new(FONTS[2]); - let bold_italic_data: Arc + Send + Sync> = Arc::new(FONTS[3]); let mut document = Document::new(); let fonts = FontFamily { - regular: Font::new(regular_data.into(), 0).unwrap(), - bold: Font::new(bold_data.into(), 0).unwrap(), - italic: Font::new(italic_data.into(), 0).unwrap(), - bold_italic: Font::new(bold_italic_data.into(), 0).unwrap(), + regular: Font::new(FONTS[0].into(), 0).unwrap(), + bold: Font::new(FONTS[1].into(), 0).unwrap(), + italic: Font::new(FONTS[2].into(), 0).unwrap(), + bold_italic: Font::new(FONTS[3].into(), 0).unwrap(), }; self.generate_pdf(&mut document, &self.paper_size, screenplay, &fonts)?; From e4015a0b9d5bd29355ad15c6e8f2fdcfcefa2578 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 1 Apr 2026 13:12:54 +0200 Subject: [PATCH 33/39] Clean up merge --- Cargo.lock | 154 ++++++++++++++---------------- crates/rustwell/src/export/pdf.rs | 8 +- 2 files changed, 79 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51d25c3..a172826 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -34,15 +34,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -114,21 +114,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -158,9 +158,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -180,9 +180,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -192,9 +192,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "color-eyre" @@ -231,9 +231,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "core_maths" @@ -261,9 +261,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "euclid" -version = "0.22.11" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", ] @@ -289,13 +289,13 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -368,9 +368,9 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -384,9 +384,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "krilla" @@ -436,24 +436,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libz-rs-sys" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" -dependencies = [ - "zlib-rs", -] +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "log" @@ -463,9 +454,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -497,9 +488,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -509,9 +500,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "pdf-writer" @@ -519,7 +510,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92a79477295a713c2ed425aa82a8b5d20cec3fdee203706cbe6f3854880c1c81" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "itoa", "memchr", "ryu", @@ -527,9 +518,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "png" @@ -546,9 +537,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -561,9 +552,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -580,23 +571,24 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustwell" version = "0.2.0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "krilla", "unicode-properties", +] [[package]] name = "rustwell-cli" @@ -613,7 +605,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "core_maths", "log", @@ -627,9 +619,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "sharded-slab" @@ -642,15 +634,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skrifa" @@ -700,9 +692,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -772,9 +764,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "sharded-slab", "thread_local", @@ -804,9 +796,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-properties" @@ -918,21 +910,21 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" [[package]] name = "zune-core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-jpeg" -version = "0.5.8" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 846c80b..861da21 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -16,7 +16,7 @@ use krilla::{ use crate::{ Exporter, Screenplay, rich_string::RichString, - screenplay::{Dialogue, DialogueElement, Element, TitlePage}, + screenplay::{Dialogue, DialogueElement, Element, Span, TitlePage}, }; const FONT_SIZE: usize = 12; // standard screenplay size @@ -300,7 +300,11 @@ impl PdfExporter { break; } - let element = &screenplay.elements[screenplay_element_idx]; + let Span { + start_line: _, + end_line: _, + inner: element, + } = &screenplay.elements[screenplay_element_idx]; let mut breakpoint_idx = match residual_breakpoint_idx { Some(i) => { // If we're in a dialogue element, we need to preserve From 054c7a1c9d56673060ab06c76e2f302183406753 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 1 Apr 2026 15:22:55 +0200 Subject: [PATCH 34/39] Combine some parameters into LayoutInfo struct --- crates/rustwell/src/export/pdf.rs | 112 ++++++++++++++---------------- 1 file changed, 53 insertions(+), 59 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 861da21..4003caa 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -171,6 +171,11 @@ const MARGINS: Margins = Margins { }, }; +struct LayoutInfo<'a> { + pub size: &'a PaperSize, + pub fonts: &'a FontFamily, +} + /// A [`Screenplay`] exporter for `pdf` /// /// The variables configure the exporter @@ -200,7 +205,12 @@ impl Exporter for PdfExporter { bold_italic: Font::new(FONTS[3].into(), 0).unwrap(), }; - self.generate_pdf(&mut document, &self.paper_size, screenplay, &fonts)?; + let layout_info = LayoutInfo { + size: &self.paper_size, + fonts: &fonts, + }; + + self.generate_pdf(&mut document, &layout_info, screenplay)?; let pdf = document .finish() @@ -221,9 +231,8 @@ impl PdfExporter { fn generate_pdf( &self, document: &mut Document, - size: &PaperSize, + layout_info: &LayoutInfo, screenplay: &Screenplay, - fonts: &FontFamily, ) -> std::io::Result<()> { // The index for which element in the screenplay is currently being processed. let mut screenplay_element_idx = 0; @@ -233,7 +242,8 @@ impl PdfExporter { // The maximum number of writable lines which can fit on a page, considering the top and // bottom margins. - let max_lines_per_page = (size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; + let max_lines_per_page = + (layout_info.size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; // If an element does not fit within a page, this will be Some(index) pointing to the which // breakpoint in the breakpoint list to start at on the next page. let mut residual_breakpoint_idx = None; @@ -246,13 +256,15 @@ impl PdfExporter { if let Some(t) = &screenplay.titlepage { page_idx += 1; - write_titlepage(size, t, max_lines_per_page, document, fonts)?; + write_titlepage(t, layout_info, max_lines_per_page, document)?; } // Page loop, creates a new page and writes everything it can on it. while screenplay_element_idx < screenplay.elements.len() { - let mut page = document - .start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); + let mut page = document.start_page_with( + PageSettings::from_wh(layout_info.size.x as f32, layout_info.size.y as f32) + .unwrap(), + ); let mut surface = page.surface(); let mut line_idx = 0; @@ -261,7 +273,7 @@ impl PdfExporter { || (screenplay.titlepage.is_some() && page_idx > 1) { let residual_page_number = write_element_custom_top_margin( - size, + layout_info, &format!( "{}.", if screenplay.titlepage.is_some() { @@ -276,7 +288,6 @@ impl PdfExporter { &mut 0, 1, &mut surface, - fonts, Alignment::RightToLeft, 36, )?; @@ -322,14 +333,13 @@ impl PdfExporter { macro_rules! write_element { ($content:expr, $margin:expr, $text_direction:expr) => { residual_breakpoint_idx = write_element( - size, + layout_info, $content, $margin, &mut breakpoint_idx, &mut line_idx, max_lines_per_page, &mut surface, - fonts, $text_direction, )? }; @@ -342,36 +352,34 @@ impl PdfExporter { let left_number_margin = Margin { left: 54.0, - right: size.x as f32 - MARGINS.heading.left + 18.0, + right: layout_info.size.x as f32 - MARGINS.heading.left + 18.0, }; let right_number_margin = Margin { - left: size.x as f32 - MARGINS.heading.right + 18.0, + left: layout_info.size.x as f32 - MARGINS.heading.right + 18.0, right: 18.0, }; let rich_number = &number.as_ref().unwrap().into(); write_element( - size, + layout_info, rich_number, &left_number_margin, &mut 0, &mut initial_line_index.clone(), max_lines_per_page, &mut surface, - fonts, Alignment::LeftToRight, )?; write_element( - size, + layout_info, rich_number, &right_number_margin, &mut 0, &mut initial_line_index.clone(), max_lines_per_page, &mut surface, - fonts, Alignment::RightToLeft, )?; } @@ -392,14 +400,13 @@ impl PdfExporter { } Element::Dialogue(dialogue) => { let premature_exit = write_dialogue( + layout_info, dialogue, &mut residual_dialogue_idx, &mut residual_breakpoint_idx, - size, max_lines_per_page, &mut line_idx, &mut surface, - fonts, &MARGINS.dialogue, )?; if residual_dialogue_idx.is_some() || premature_exit { @@ -417,14 +424,13 @@ impl PdfExporter { { premature_exit = premature_exit || write_dialogue( + layout_info, dialogue0, &mut residual_dual_dialogue_idx.0, &mut residual_dual_breakpoint_idx.0, - size, max_lines_per_page, &mut line_idx, &mut surface, - fonts, &MARGINS.dual_dialogue.left, )?; } @@ -434,14 +440,13 @@ impl PdfExporter { { premature_exit = premature_exit || write_dialogue( + layout_info, dialogue1, &mut residual_dual_dialogue_idx.1, &mut residual_dual_breakpoint_idx.1, - size, max_lines_per_page, &mut initial_line_index, &mut surface, - fonts, &MARGINS.dual_dialogue.right, )?; } @@ -502,14 +507,13 @@ impl PdfExporter { /// write the character name with the extension `(cont'd)` on each new page. Returns a /// [Option] which is true if the whole dialogue element did not fit on the same page. fn write_dialogue( + layout_info: &LayoutInfo, dialogue: &Dialogue, residual_dialogue: &mut Option, residual_index: &mut Option, - size: &PaperSize, max_lines: usize, line_index: &mut usize, surface: &mut Surface, - fonts: &FontFamily, dialogue_margins: &DialogueMargins, ) -> std::io::Result { let mut character_name = dialogue.character.clone(); @@ -525,7 +529,7 @@ fn write_dialogue( _ => (), }; let span = glyph_span( - size, + layout_info.size, dialogue_margins.character.left, dialogue_margins.character.right, ); @@ -543,14 +547,13 @@ fn write_dialogue( } write_element( - size, + layout_info, &character_name, &dialogue_margins.character, &mut 0, line_index, max_lines, surface, - fonts, Alignment::LeftToRight, )?; @@ -561,14 +564,13 @@ fn write_dialogue( // If a dialogue continues on the next page, writes `(MORE)` at the bottom of the // current page, inside the bottom margin. write_element( - size, + layout_info, &"(MORE)".into(), &dialogue_margins.character, &mut 0, line_index, max_lines + 1, surface, - fonts, Alignment::LeftToRight, )?; @@ -588,14 +590,13 @@ fn write_dialogue( }; *residual_index = write_element( - size, + layout_info, content, margin, &mut breakpoint_index, line_index, max_lines, surface, - fonts, Alignment::LeftToRight, )?; @@ -614,25 +615,23 @@ fn write_dialogue( /// will return [`Some`] with an index to which [`BreakPoint`] the function made it to. Calls /// [`write_element_custom_top_margin`] with the standard top margin. fn write_element( - size: &PaperSize, + layout_info: &LayoutInfo, content: &RichString, margin: &Margin, breakpoint_index: &mut usize, line_index: &mut usize, max_lines: usize, surface: &mut Surface, - fonts: &FontFamily, text_direction: Alignment, ) -> std::io::Result> { write_element_custom_top_margin( - size, + layout_info, content, margin, breakpoint_index, line_index, max_lines, surface, - fonts, text_direction, TOP_MARGIN, ) @@ -642,20 +641,19 @@ fn write_element( /// will return [`Some`] with an index to which [`BreakPoint`] the function made it to. Allows for /// using a non-default top margin (for page numbers). fn write_element_custom_top_margin( - size: &PaperSize, + layout_info: &LayoutInfo, content: &RichString, margin: &Margin, breakpoint_index: &mut usize, line_index: &mut usize, max_lines: usize, surface: &mut Surface, - fonts: &FontFamily, text_direction: Alignment, top_margin: usize, ) -> std::io::Result> { let left_margin = margin.left; let right_margin = margin.right; - let span = glyph_span(size, left_margin, right_margin); + let span = glyph_span(layout_info.size, left_margin, right_margin); let breakpoints = break_points(content, span); while *breakpoint_index <= breakpoints.len() { if *line_index >= max_lines { @@ -668,15 +666,14 @@ fn write_element_custom_top_margin( breakpoints[*breakpoint_index - 1].index }; write_line( + layout_info, surface, left_margin, (FONT_SIZE * *line_index + top_margin) as f32, content, start_index, breakpoints.get(*breakpoint_index), - fonts, text_direction, - size, margin, )?; *breakpoint_index += 1; @@ -687,15 +684,14 @@ fn write_element_custom_top_margin( /// Writes a single line (not to be confused with [`DialogueElement::Line`]) onto the page. fn write_line( + layout_info: &LayoutInfo, surface: &mut Surface, mut x: f32, y: f32, content: &RichString, mut start_index: usize, // the "global" index to the rich string breakpoint: Option<&BreakPoint>, - fonts: &FontFamily, text_direction: Alignment, - size: &PaperSize, margin: &Margin, ) -> std::io::Result<()> { match content.get_char(start_index) { @@ -726,12 +722,12 @@ fn write_line( Alignment::RightToLeft => { let line_length = breakpoint_index - start_index; let line_span = line_length as f32 * FONT_WIDTH; - x += size.x as f32 - (margin.left + margin.right) - line_span; + x += layout_info.size.x as f32 - (margin.left + margin.right) - line_span; } Alignment::Centered => { let line_length = breakpoint_index - start_index; let line_span = (line_length / 2) as f32 * FONT_WIDTH; - x = (size.x / 2) as f32 - line_span; + x = (layout_info.size.x / 2) as f32 - line_span; } } @@ -759,10 +755,10 @@ fn write_line( breakpoint_index - (start_index - relative_index) }; let font = match (string_element.is_bold(), string_element.is_italic()) { - (false, false) => &fonts.regular, - (true, false) => &fonts.bold, - (false, true) => &fonts.italic, - (true, true) => &fonts.bold_italic, + (false, false) => &layout_info.fonts.regular, + (true, false) => &layout_info.fonts.bold, + (false, true) => &layout_info.fonts.italic, + (true, true) => &layout_info.fonts.bold_italic, }; let mut char_indices = string_element.text.char_indices(); let start_byte_index = char_indices.nth(relative_index).unwrap().0; @@ -808,7 +804,7 @@ fn write_line( if break_word { surface.draw_text( Point::from_xy(x + (glyph_index as f32 * FONT_WIDTH), y), - fonts.regular.clone(), + layout_info.fonts.regular.clone(), FONT_SIZE as f32, "-", false, @@ -860,14 +856,14 @@ const TITLE_PAGE_MARGINS: TitlePageMargins = TitlePageMargins { /// Writes the [`TitlePage`] to the `pdf` document. Fails if everything does not fit on one page, /// but allows for overlapping contact information and draft dates with the rest of the elements. fn write_titlepage( - size: &PaperSize, titlepage: &TitlePage, + layout_info: &LayoutInfo, max_lines: usize, document: &mut Document, - fonts: &FontFamily, ) -> std::io::Result<()> { - let mut page = - document.start_page_with(PageSettings::from_wh(size.x as f32, size.y as f32).unwrap()); + let mut page = document.start_page_with( + PageSettings::from_wh(layout_info.size.x as f32, layout_info.size.y as f32).unwrap(), + ); let mut surface = page.surface(); let mut line_idx = max_lines / 3; @@ -880,14 +876,13 @@ fn write_titlepage( if !titlepage.$element.is_empty() { for s in &titlepage.$element { let residual = write_element( - size, + layout_info, s, &TITLE_PAGE_MARGINS.$element, &mut 0, &mut line_idx, max_lines, &mut surface, - fonts, Alignment::Centered, )?; @@ -909,7 +904,7 @@ fn write_titlepage( total_lines += break_points( s, glyph_span( - size, + layout_info.size, TITLE_PAGE_MARGINS.$element.left, TITLE_PAGE_MARGINS.$element.right, ), @@ -927,14 +922,13 @@ fn write_titlepage( for s in &titlepage.$element { write_element( - size, + layout_info, s, &TITLE_PAGE_MARGINS.$element, &mut 0, &mut line_idx, max_lines, &mut surface, - fonts, $alignment, )?; } From b7cf3ffb5f1e126a6ee8604214ddc4425ba9146a Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 1 Apr 2026 15:51:11 +0200 Subject: [PATCH 35/39] Clean up code a bit --- crates/rustwell/src/export/pdf.rs | 34 +++++++++++++----------------- crates/rustwell/src/rich_string.rs | 22 ++++++++++++++----- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 4003caa..b964dfa 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -234,8 +234,7 @@ impl PdfExporter { layout_info: &LayoutInfo, screenplay: &Screenplay, ) -> std::io::Result<()> { - // The index for which element in the screenplay is currently being processed. - let mut screenplay_element_idx = 0; + let mut element_iter = screenplay.elements.iter().peekable(); // The index for which page in the document is currently being written. let mut page_idx = 0; @@ -244,8 +243,8 @@ impl PdfExporter { // bottom margins. let max_lines_per_page = (layout_info.size.y - (TOP_MARGIN + BOTTOM_MARGIN)) / FONT_SIZE - 1; - // If an element does not fit within a page, this will be Some(index) pointing to the which - // breakpoint in the breakpoint list to start at on the next page. + // If an element does not fit within a page this will be Some(index), where index is pointing + // to the breakpoint in the breakpoint list which should be on the start of the next page. let mut residual_breakpoint_idx = None; let mut residual_dialogue_idx = None; @@ -260,7 +259,7 @@ impl PdfExporter { } // Page loop, creates a new page and writes everything it can on it. - while screenplay_element_idx < screenplay.elements.len() { + while element_iter.peek().is_some() { let mut page = document.start_page_with( PageSettings::from_wh(layout_info.size.x as f32, layout_info.size.y as f32) .unwrap(), @@ -302,20 +301,16 @@ impl PdfExporter { } // Element loop, iterates through the screenplay elements. - loop { + while let Some(Span { + start_line: _, + end_line: _, + inner: element, + }) = element_iter.peek() + { if line_idx >= max_lines_per_page { break; } - if screenplay_element_idx >= screenplay.elements.len() { - break; - } - - let Span { - start_line: _, - end_line: _, - inner: element, - } = &screenplay.elements[screenplay_element_idx]; let mut breakpoint_idx = match residual_breakpoint_idx { Some(i) => { // If we're in a dialogue element, we need to preserve @@ -479,18 +474,19 @@ impl PdfExporter { } } Element::PageBreak => { - screenplay_element_idx += 1; + element_iter.next(); break; } } + // Newline separator between all elements line_idx += 1; if residual_breakpoint_idx.is_some() { continue; } - screenplay_element_idx += 1; + element_iter.next(); } surface.finish(); @@ -712,7 +708,7 @@ fn write_line( let (breakpoint_index, break_word) = match breakpoint { Some(b) => (b.index, b.break_type == BreakType::BreakWord), - None => (content.len(), false), + None => (content.char_count(), false), }; // If we use a nonstandard [`Alignment`] we must move the `x` value of which we start writing @@ -980,7 +976,7 @@ struct BreakPoint { fn break_points(content: &RichString, span: usize) -> Vec { debug_assert!(span >= 2); - let mut brekpoints = Vec::with_capacity(content.len() / span + 1); + let mut brekpoints = Vec::with_capacity(content.char_count() / span + 1); let mut last_whitespace_char = (0, 0); let mut line_len = 0; for (i, glyph) in content.iter().enumerate() { diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 3cc8575..8f5c3f8 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -19,8 +19,8 @@ //! assert!(rs.elements[2].is_bold()); //! ``` -use std::{fmt::Display, str::Chars}; use std::collections::HashMap; +use std::{fmt::Display, str::Chars}; use bitflags::bitflags; use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory}; @@ -66,7 +66,7 @@ impl RichString { } /// The total length of a [RichString], meaning the total number of [char]s. - pub fn len(&self) -> usize { + pub fn char_count(&self) -> usize { let mut len = 0; for e in &self.elements { len += e.text.chars().count(); @@ -76,8 +76,20 @@ impl RichString { /// Gets a [`char`] from a "global" index, meaning the index when viewing the [`RichString`] as /// a single string without any style attributes taken into account. + /// + /// # Examples + /// + /// ``` + /// use rustwell::rich_string::RichString; + /// + /// let mut rs = RichString::from("He**llo**"); + /// + /// assert_eq!(rs.get_char(1), Some('e')); + /// assert_eq!(rs.get_char(3), Some('l')); + /// assert_eq!(rs.get_char(5), None); + /// ``` pub fn get_char(&self, mut index: usize) -> Option { - if index >= self.len() { + if index >= self.char_count() { return None; } for e in &self.elements { @@ -93,7 +105,7 @@ impl RichString { /// Given a "global" index, gets the [`Element`] which contains it, and the "local" index /// pointing to that character in the element. pub fn get_element_from_index(&self, mut index: usize) -> Option<(&Element, usize)> { - if index >= self.len() { + if index >= self.char_count() { return None; } for e in &self.elements { @@ -375,7 +387,7 @@ impl Default for RichString { impl Display for RichString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut str = String::with_capacity(self.len()); + let mut str = String::with_capacity(self.char_count()); for element in &self.elements { str.push_str(&element.text); } From 07ab40e9432ec16d107e88439ceae1c8ecb44253 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist <104637174+frblo@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:18:44 +0200 Subject: [PATCH 36/39] Update crates/rustwell/src/rich_string.rs Co-authored-by: Emil Hultcrantz <90456354+Frequinzy@users.noreply.github.com> --- crates/rustwell/src/rich_string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 8f5c3f8..30a9fbb 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -65,7 +65,7 @@ impl RichString { } } - /// The total length of a [RichString], meaning the total number of [char]s. + /// The total length of a [`RichString`], meaning the total number of [`char`]s. pub fn char_count(&self) -> usize { let mut len = 0; for e in &self.elements { From c4f1311edef6da9a6b095eab7d7b0041ad125385 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 1 Apr 2026 21:38:32 +0200 Subject: [PATCH 37/39] Fix feedback --- crates/rustwell/src/export/pdf.rs | 2 +- crates/rustwell/src/rich_string.rs | 81 ++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index b964dfa..690f742 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -379,7 +379,7 @@ impl PdfExporter { )?; } outline.push_child(OutlineNode::new( - slug.to_string(), + slug.to_plain_string(), XyzDestination::new( page_idx, Point { diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 30a9fbb..546e644 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -104,6 +104,19 @@ impl RichString { /// Given a "global" index, gets the [`Element`] which contains it, and the "local" index /// pointing to that character in the element. + /// + /// # Examples + /// + /// ``` + /// use rustwell::rich_string::RichString; + /// + /// let mut rs = RichString::from("He**ll**o"); + /// + /// assert!(matches!(rs.get_element_from_index(1), Some((_, 1)))); + /// assert!(matches!(rs.get_element_from_index(2), Some((_, 0)))); + /// assert!(matches!(rs.get_element_from_index(3), Some((_, 1)))); + /// assert!(matches!(rs.get_element_from_index(4), Some((_, 0)))); + /// ``` pub fn get_element_from_index(&self, mut index: usize) -> Option<(&Element, usize)> { if index >= self.char_count() { return None; @@ -128,9 +141,19 @@ impl RichString { } } - /// Appends a [`RichString`] to self. Will not merge the last [`Element`] of self, and the - /// first of the other even if they have the same style attributes. + /// Appends a [`RichString`] to self. Will merge the last [`Element`] of self, and the + /// first of the other if they have the same style attributes. pub fn append(&mut self, mut other: Self) { + if let Some(e) = other.elements.first() + && let Some(l) = self.elements.last_mut() + { + if e.attributes == l.attributes { + l.text.push_str(&e.text); + other.elements.drain(..1); + self.elements.append(&mut other.elements); + return; + } + } self.elements.append(&mut other.elements); } @@ -143,6 +166,27 @@ impl RichString { self.push_parsed(&tokens, &delimiters, &matches); } + /// Converts the [`RichString`] to a plain [`String`] by just combining the string elements + /// without adding delimiters. + /// + /// # Examples + /// + /// ``` + /// use rustwell::rich_string::RichString; + /// + /// let mut rs = RichString::from("He**ll**o"); + /// + /// assert_eq!(rs.to_plain_string(), "Hello".to_string()); + /// ``` + pub fn to_plain_string(&self) -> String { + let mut str = String::with_capacity(self.char_count()); + for element in &self.elements { + str.push_str(&element.text); + } + + str + } + /// Creates a list of "tokens" and [`Delimiter`] runs. /// /// By token is meant [`&str`] slices divided at delimiters. @@ -388,8 +432,25 @@ impl Default for RichString { impl Display for RichString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut str = String::with_capacity(self.char_count()); + for element in &self.elements { - str.push_str(&element.text); + macro_rules! attr_to_delim { + ($attr:ident, $delimiter:expr) => { + if element.$attr() { $delimiter } else { "" } + }; + } + + let element_text = format!( + "{}{}{}{}{}{}{}", + attr_to_delim!(is_bold, "**"), + attr_to_delim!(is_italic, "*"), + attr_to_delim!(is_underline, "_"), + element.text, + attr_to_delim!(is_underline, "_"), + attr_to_delim!(is_italic, "*"), + attr_to_delim!(is_bold, "**"), + ); + str.extend(element_text.chars()); } write!(f, "{str}") @@ -503,6 +564,20 @@ struct Match { mod tests { use super::*; + #[test] + fn appends_same_attributes() { + let mut rs: RichString = "a*b*".into(); + let other: RichString = "*c*d".into(); + rs.append(other); + assert_eq!(rs.elements[1].text, "bc".to_string()) + } + + #[test] + fn displays_with_delims() { + let rs: RichString = "H**e**_ll_**_o_**".into(); + assert_eq!(rs, rs.to_string().into()) + } + mod tokenize { use super::*; From 0b65b8cfd0579a6975e54f8a35251054091ee6a5 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 1 Apr 2026 22:06:13 +0200 Subject: [PATCH 38/39] CONT'D should be capitalized --- crates/rustwell/src/export/pdf.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rustwell/src/export/pdf.rs b/crates/rustwell/src/export/pdf.rs index 690f742..ad84cc4 100644 --- a/crates/rustwell/src/export/pdf.rs +++ b/crates/rustwell/src/export/pdf.rs @@ -500,7 +500,7 @@ impl PdfExporter { } /// Writes a diologue [`Element`] to the `pdf` document. If a dialogue spans multiple pages it will -/// write the character name with the extension `(cont'd)` on each new page. Returns a +/// write the character name with the extension `(CONT'D)` on each new page. Returns a /// [Option] which is true if the whole dialogue element did not fit on the same page. fn write_dialogue( layout_info: &LayoutInfo, @@ -515,7 +515,7 @@ fn write_dialogue( let mut character_name = dialogue.character.clone(); match (*residual_dialogue, &dialogue.extension) { (Some(_), _) => { - character_name.append(" (cont'd)".into()); + character_name.append(" (CONT'D)".into()); } (None, Some(ext)) => { character_name.append(" (".into()); From f263b961b157f3d2734908bae9787dda6022bc43 Mon Sep 17 00:00:00 2001 From: Fredrik Blomqvist Date: Wed, 1 Apr 2026 22:15:10 +0200 Subject: [PATCH 39/39] Fix a comment --- crates/rustwell/src/rich_string.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rustwell/src/rich_string.rs b/crates/rustwell/src/rich_string.rs index 546e644..b4c409c 100644 --- a/crates/rustwell/src/rich_string.rs +++ b/crates/rustwell/src/rich_string.rs @@ -131,8 +131,8 @@ impl RichString { None } - /// Creates an [char] iterator over the [`RichString`], without the style attributes of each - /// [char] taken into account. + /// Creates an [`char`] iterator over the [`RichString`], without the style attributes of each + /// [`char`] taken into account. pub fn iter(&'_ self) -> RichIterator<'_> { RichIterator { rich_string: self,