use crate::error::Result;
use crate::graphics::Color;
use crate::page::Margins;
use crate::text::metrics::{measure_text_with, FontMetricsStore};
use crate::text::{split_into_words, Font};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextAlign {
Left,
Right,
Center,
Justified,
}
pub struct TextFlowContext {
operations: Vec<crate::graphics::ops::Op>,
current_font: Font,
font_size: f64,
line_height: f64,
cursor_x: f64,
cursor_y: f64,
alignment: TextAlign,
page_width: f64,
#[allow(dead_code)]
page_height: f64,
margins: Margins,
fill_color: Option<Color>,
character_spacing: Option<f64>,
word_spacing: Option<f64>,
horizontal_scaling: Option<f64>,
leading: Option<f64>,
text_rise: Option<f64>,
rendering_mode: Option<u8>,
stroke_color: Option<Color>,
used_characters_by_font: HashMap<String, HashSet<char>>,
pub(crate) font_metrics_store: Option<FontMetricsStore>,
}
impl TextFlowContext {
pub fn new(page_width: f64, page_height: f64, margins: Margins) -> Self {
Self {
operations: Vec::new(),
current_font: Font::Helvetica,
font_size: 12.0,
line_height: 1.2,
cursor_x: margins.left,
cursor_y: page_height - margins.top,
alignment: TextAlign::Left,
page_width,
page_height,
margins,
fill_color: None,
character_spacing: None,
word_spacing: None,
horizontal_scaling: None,
leading: None,
text_rise: None,
rendering_mode: None,
stroke_color: None,
used_characters_by_font: HashMap::new(),
font_metrics_store: None,
}
}
pub(crate) fn with_metrics_store(
page_width: f64,
page_height: f64,
margins: Margins,
store: Option<FontMetricsStore>,
) -> Self {
let mut ctx = Self::new(page_width, page_height, margins);
ctx.font_metrics_store = store;
ctx
}
pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
&self.used_characters_by_font
}
pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
self.current_font = font;
self.font_size = size;
self
}
pub fn set_line_height(&mut self, multiplier: f64) -> &mut Self {
self.line_height = multiplier;
self
}
pub fn set_alignment(&mut self, alignment: TextAlign) -> &mut Self {
self.alignment = alignment;
self
}
pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
self.fill_color = Some(color);
self
}
pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
self.character_spacing = Some(spacing);
self
}
pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
self.word_spacing = Some(spacing);
self
}
pub fn set_horizontal_scaling(&mut self, scale: f64) -> &mut Self {
self.horizontal_scaling = Some(scale);
self
}
pub fn set_leading(&mut self, leading: f64) -> &mut Self {
self.leading = Some(leading);
self
}
pub fn set_text_rise(&mut self, rise: f64) -> &mut Self {
self.text_rise = Some(rise);
self
}
pub fn set_rendering_mode(&mut self, mode: u8) -> &mut Self {
self.rendering_mode = Some(mode);
self
}
pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
self.stroke_color = Some(color);
self
}
pub fn current_font(&self) -> &Font {
&self.current_font
}
pub fn font_size(&self) -> f64 {
self.font_size
}
pub fn fill_color(&self) -> Option<Color> {
self.fill_color
}
pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
self.cursor_x = x;
self.cursor_y = y;
self
}
pub fn content_width(&self) -> f64 {
self.page_width - self.margins.left - self.margins.right
}
pub fn available_width(&self) -> f64 {
(self.page_width - self.margins.right - self.cursor_x).max(0.0)
}
pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
let start_x = self.cursor_x;
let available_width = self.available_width();
let words = split_into_words(text);
let mut lines: Vec<Vec<&str>> = Vec::new();
let mut current_line: Vec<&str> = Vec::new();
let mut current_width = 0.0;
for word in words {
let word_width = measure_text_with(
word,
&self.current_font,
self.font_size,
self.font_metrics_store.as_ref(),
);
if !current_line.is_empty() && current_width + word_width > available_width {
lines.push(current_line);
current_line = vec![word];
current_width = word_width;
} else {
current_line.push(word);
current_width += word_width;
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
for (i, line) in lines.iter().enumerate() {
let line_text = line.join("");
let line_width = measure_text_with(
&line_text,
&self.current_font,
self.font_size,
self.font_metrics_store.as_ref(),
);
let x = match self.alignment {
TextAlign::Left => start_x,
TextAlign::Right => self.page_width - self.margins.right - line_width,
TextAlign::Center => start_x + (available_width - line_width) / 2.0,
TextAlign::Justified => start_x,
};
use crate::graphics::ops::Op;
self.operations.push(Op::BeginText);
self.operations.push(Op::SetFont {
name: self.current_font.pdf_name(),
size: self.font_size,
});
if let Some(spacing) = self.character_spacing {
self.operations.push(Op::SetCharSpacing(spacing));
}
if let Some(spacing) = self.word_spacing {
self.operations.push(Op::SetWordSpacing(spacing));
}
if let Some(scale) = self.horizontal_scaling {
self.operations
.push(Op::SetHorizontalScaling(scale * 100.0));
}
if let Some(leading) = self.leading {
self.operations.push(Op::SetLeading(leading));
}
if let Some(rise) = self.text_rise {
self.operations.push(Op::SetTextRise(rise));
}
if let Some(mode) = self.rendering_mode {
self.operations.push(Op::SetRenderingMode(mode));
}
if let Some(color) = self.fill_color {
self.operations.push(Op::SetFillColor(color));
}
if let Some(color) = self.stroke_color {
self.operations.push(Op::SetStrokeColor(color));
}
self.operations.push(Op::SetTextPosition {
x,
y: self.cursor_y,
});
if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
if spaces_count > 0 {
let extra_space = available_width - line_width;
let space_adjustment = extra_space / spaces_count as f64;
self.operations.push(Op::SetWordSpacing(space_adjustment));
}
}
let mut buf = Vec::with_capacity(line_text.len());
for ch in line_text.chars() {
match ch {
'(' => buf.extend_from_slice(b"\\("),
')' => buf.extend_from_slice(b"\\)"),
'\\' => buf.extend_from_slice(b"\\\\"),
'\n' => buf.extend_from_slice(b"\\n"),
'\r' => buf.extend_from_slice(b"\\r"),
'\t' => buf.extend_from_slice(b"\\t"),
_ => {
let mut tmp = [0u8; 4];
buf.extend_from_slice(ch.encode_utf8(&mut tmp).as_bytes());
}
}
}
self.operations.push(Op::ShowText(buf));
self.used_characters_by_font
.entry(self.current_font.pdf_name())
.or_default()
.extend(line_text.chars());
if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
self.operations.push(Op::SetWordSpacing(0.0));
}
self.operations.push(Op::EndText);
self.cursor_y -= self.font_size * self.line_height;
}
Ok(self)
}
pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
self.write_wrapped(text)?;
self.cursor_y -= self.font_size * self.line_height * 0.5;
Ok(self)
}
pub fn newline(&mut self) -> &mut Self {
self.cursor_y -= self.font_size * self.line_height;
self.cursor_x = self.margins.left;
self
}
pub fn cursor_position(&self) -> (f64, f64) {
(self.cursor_x, self.cursor_y)
}
pub fn generate_operations(&self) -> Vec<u8> {
let mut buf = Vec::new();
crate::graphics::ops::serialize_ops(&mut buf, &self.operations);
buf
}
pub fn alignment(&self) -> TextAlign {
self.alignment
}
pub fn page_dimensions(&self) -> (f64, f64) {
(self.page_width, self.page_height)
}
pub fn margins(&self) -> &Margins {
&self.margins
}
pub fn line_height(&self) -> f64 {
self.line_height
}
pub fn operations(&self) -> String {
crate::graphics::ops::ops_to_string(&self.operations)
}
pub fn clear(&mut self) {
self.operations.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page::Margins;
fn create_test_margins() -> Margins {
Margins {
left: 50.0,
right: 50.0,
top: 50.0,
bottom: 50.0,
}
}
#[test]
fn test_text_flow_context_new() {
let margins = create_test_margins();
let context = TextFlowContext::new(400.0, 600.0, margins);
assert_eq!(context.current_font, Font::Helvetica);
assert_eq!(context.font_size, 12.0);
assert_eq!(context.line_height, 1.2);
assert_eq!(context.alignment, TextAlign::Left);
assert_eq!(context.page_width, 400.0);
assert_eq!(context.page_height, 600.0);
assert_eq!(context.cursor_x, 50.0); assert_eq!(context.cursor_y, 550.0); }
#[test]
fn test_set_font() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_font(Font::TimesBold, 16.0);
assert_eq!(context.current_font, Font::TimesBold);
assert_eq!(context.font_size, 16.0);
}
#[test]
fn test_set_line_height() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_line_height(1.5);
assert_eq!(context.line_height(), 1.5);
}
#[test]
fn test_set_alignment() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_alignment(TextAlign::Center);
assert_eq!(context.alignment(), TextAlign::Center);
}
#[test]
fn test_at_position() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.at(100.0, 200.0);
let (x, y) = context.cursor_position();
assert_eq!(x, 100.0);
assert_eq!(y, 200.0);
}
#[test]
fn test_content_width() {
let margins = create_test_margins();
let context = TextFlowContext::new(400.0, 600.0, margins);
let content_width = context.content_width();
assert_eq!(content_width, 300.0); }
#[test]
fn test_text_align_variants() {
assert_eq!(TextAlign::Left, TextAlign::Left);
assert_eq!(TextAlign::Right, TextAlign::Right);
assert_eq!(TextAlign::Center, TextAlign::Center);
assert_eq!(TextAlign::Justified, TextAlign::Justified);
assert_ne!(TextAlign::Left, TextAlign::Right);
}
#[test]
fn test_write_wrapped_simple() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped("Hello World").unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n"));
assert!(ops.contains("ET\n"));
assert!(ops.contains("/Helvetica 12 Tf"));
assert!(ops.contains("(Hello World) Tj"));
}
#[test]
fn test_write_paragraph() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let initial_y = context.cursor_y;
context.write_paragraph("Test paragraph").unwrap();
assert!(context.cursor_y < initial_y);
}
#[test]
fn test_newline() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
let initial_y = context.cursor_y;
context.newline();
assert_eq!(context.cursor_x, margins.left);
assert!(context.cursor_y < initial_y);
assert_eq!(
context.cursor_y,
initial_y - context.font_size * context.line_height
);
}
#[test]
fn test_cursor_position() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.at(75.0, 125.0);
let (x, y) = context.cursor_position();
assert_eq!(x, 75.0);
assert_eq!(y, 125.0);
}
#[test]
fn test_generate_operations() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped("Test").unwrap();
let ops_bytes = context.generate_operations();
let ops_string = String::from_utf8(ops_bytes).unwrap();
assert_eq!(ops_string, context.operations());
}
#[test]
fn test_clear_operations() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped("Test").unwrap();
assert!(!context.operations().is_empty());
context.clear();
assert!(context.operations().is_empty());
}
#[test]
fn test_page_dimensions() {
let margins = create_test_margins();
let context = TextFlowContext::new(400.0, 600.0, margins);
let (width, height) = context.page_dimensions();
assert_eq!(width, 400.0);
assert_eq!(height, 600.0);
}
#[test]
fn test_margins_access() {
let margins = create_test_margins();
let context = TextFlowContext::new(400.0, 600.0, margins);
let ctx_margins = context.margins();
assert_eq!(ctx_margins.left, 50.0);
assert_eq!(ctx_margins.right, 50.0);
assert_eq!(ctx_margins.top, 50.0);
assert_eq!(ctx_margins.bottom, 50.0);
}
#[test]
fn test_method_chaining() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context
.set_font(Font::Courier, 10.0)
.set_line_height(1.5)
.set_alignment(TextAlign::Center)
.at(100.0, 200.0);
assert_eq!(context.current_font, Font::Courier);
assert_eq!(context.font_size, 10.0);
assert_eq!(context.line_height(), 1.5);
assert_eq!(context.alignment(), TextAlign::Center);
let (x, y) = context.cursor_position();
assert_eq!(x, 100.0);
assert_eq!(y, 200.0);
}
#[test]
fn test_text_align_debug() {
let align = TextAlign::Center;
let debug_str = format!("{align:?}");
assert_eq!(debug_str, "Center");
}
#[test]
fn test_text_align_clone() {
let align1 = TextAlign::Justified;
let align2 = align1;
assert_eq!(align1, align2);
}
#[test]
fn test_text_align_copy() {
let align1 = TextAlign::Right;
let align2 = align1; assert_eq!(align1, align2);
assert_eq!(align1, TextAlign::Right);
assert_eq!(align2, TextAlign::Right);
}
#[test]
fn test_write_wrapped_with_alignment_right() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_alignment(TextAlign::Right);
context.write_wrapped("Right aligned text").unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n"));
assert!(ops.contains("ET\n"));
assert!(ops.contains("Td"));
}
#[test]
fn test_write_wrapped_with_alignment_center() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_alignment(TextAlign::Center);
context.write_wrapped("Centered text").unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n"));
assert!(ops.contains("(Centered text) Tj"));
}
#[test]
fn test_write_wrapped_with_alignment_justified() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_alignment(TextAlign::Justified);
context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n"));
assert!(ops.contains("Tw") || ops.contains("0 Tw"));
}
#[test]
fn test_write_wrapped_empty_text() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped("").unwrap();
assert!(context.operations().is_empty());
}
#[test]
fn test_write_wrapped_whitespace_only() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped(" ").unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n") || ops.is_empty());
}
#[test]
fn test_write_wrapped_special_characters() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context
.write_wrapped("Text with (parentheses) and \\backslash\\")
.unwrap();
let ops = context.operations();
assert!(ops.contains("\\(parentheses\\)"));
assert!(ops.contains("\\\\backslash\\\\"));
}
#[test]
fn test_write_wrapped_newlines_tabs() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
let ops = context.operations();
assert!(ops.contains("\\n") || ops.contains("\\t"));
}
#[test]
fn test_write_wrapped_very_long_word() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(200.0, 600.0, margins);
let long_word = "a".repeat(100);
context.write_wrapped(&long_word).unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n"));
assert!(ops.contains(&long_word));
}
#[test]
fn test_write_wrapped_cursor_movement() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let initial_y = context.cursor_y;
context.write_wrapped("Line 1").unwrap();
let y_after_line1 = context.cursor_y;
context.write_wrapped("Line 2").unwrap();
let y_after_line2 = context.cursor_y;
assert!(y_after_line1 < initial_y);
assert!(y_after_line2 < y_after_line1);
}
#[test]
fn test_write_paragraph_spacing() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let initial_y = context.cursor_y;
context.write_paragraph("Paragraph 1").unwrap();
let y_after_p1 = context.cursor_y;
context.write_paragraph("Paragraph 2").unwrap();
let y_after_p2 = context.cursor_y;
let spacing1 = initial_y - y_after_p1;
let spacing2 = y_after_p1 - y_after_p2;
assert!(spacing1 > 0.0);
assert!(spacing2 > 0.0);
}
#[test]
fn test_multiple_newlines() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let initial_y = context.cursor_y;
context.newline();
let y1 = context.cursor_y;
context.newline();
let y2 = context.cursor_y;
context.newline();
let y3 = context.cursor_y;
let spacing1 = initial_y - y1;
let spacing2 = y1 - y2;
let spacing3 = y2 - y3;
assert!((spacing1 - spacing2).abs() < 1e-10);
assert!((spacing2 - spacing3).abs() < 1e-10);
assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
}
#[test]
fn test_content_width_different_margins() {
let margins = Margins {
left: 30.0,
right: 70.0,
top: 40.0,
bottom: 60.0,
};
let context = TextFlowContext::new(500.0, 700.0, margins);
let content_width = context.content_width();
assert_eq!(content_width, 400.0); }
#[test]
fn test_custom_line_height() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_line_height(2.0);
let initial_y = context.cursor_y;
context.newline();
let y_after = context.cursor_y;
let spacing = initial_y - y_after;
assert_eq!(spacing, context.font_size * 2.0); }
#[test]
fn test_different_fonts() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let fonts = vec![
Font::Helvetica,
Font::HelveticaBold,
Font::TimesRoman,
Font::TimesBold,
Font::Courier,
Font::CourierBold,
];
for font in fonts {
context.clear();
let font_name = font.pdf_name();
context.set_font(font, 14.0);
context.write_wrapped("Test text").unwrap();
let ops = context.operations();
assert!(ops.contains(&format!("/{font_name} 14 Tf")));
}
}
#[test]
fn test_font_size_variations() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
for size in sizes {
context.clear();
context.set_font(Font::Helvetica, size);
context.write_wrapped("Test").unwrap();
let ops = context.operations();
assert!(ops.contains(&format!("/Helvetica {size} Tf")));
}
}
#[test]
fn test_at_position_edge_cases() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.at(0.0, 0.0);
assert_eq!(context.cursor_position(), (0.0, 0.0));
context.at(-10.0, -20.0);
assert_eq!(context.cursor_position(), (-10.0, -20.0));
context.at(10000.0, 20000.0);
assert_eq!(context.cursor_position(), (10000.0, 20000.0));
}
#[test]
fn test_write_wrapped_with_narrow_content() {
let margins = Margins {
left: 190.0,
right: 190.0,
top: 50.0,
bottom: 50.0,
};
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context
.write_wrapped("This text should wrap a lot")
.unwrap();
let ops = context.operations();
let bt_count = ops.matches("BT\n").count();
assert!(bt_count > 1);
}
#[test]
fn test_justified_text_single_word_line() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_alignment(TextAlign::Justified);
context.write_wrapped("SingleWord").unwrap();
let ops = context.operations();
assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
}
#[test]
fn test_justified_text_last_line() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_alignment(TextAlign::Justified);
context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
let ops = context.operations();
assert!(ops.contains("0 Tw"));
}
#[test]
fn test_generate_operations_encoding() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.write_wrapped("UTF-8 Text: Ñ").unwrap();
let ops_bytes = context.generate_operations();
let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
assert_eq!(ops_bytes, context.operations().as_bytes());
assert_eq!(ops_string, context.operations());
}
#[test]
fn test_clear_resets_operations_only() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_font(Font::TimesBold, 18.0);
context.set_alignment(TextAlign::Right);
context.at(100.0, 200.0);
context.write_wrapped("Text").unwrap();
context.clear();
assert!(context.operations().is_empty());
assert_eq!(context.current_font, Font::TimesBold);
assert_eq!(context.font_size, 18.0);
assert_eq!(context.alignment(), TextAlign::Right);
let (x, y) = context.cursor_position();
assert_eq!(x, 100.0); assert!(y < 200.0); }
#[test]
fn test_long_text_wrapping() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
context.write_wrapped(long_text).unwrap();
let ops = context.operations();
let tj_count = ops.matches(") Tj").count();
assert!(tj_count > 1);
}
#[test]
fn test_empty_operations_initially() {
let margins = create_test_margins();
let context = TextFlowContext::new(400.0, 600.0, margins);
assert!(context.operations().is_empty());
assert_eq!(context.generate_operations().len(), 0);
}
#[test]
fn test_write_paragraph_empty() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
let initial_y = context.cursor_y;
context.write_paragraph("").unwrap();
assert!(context.cursor_y < initial_y);
}
#[test]
fn test_extreme_line_height() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_line_height(0.1);
let initial_y = context.cursor_y;
context.newline();
assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
context.set_line_height(10.0);
let initial_y2 = context.cursor_y;
context.newline();
assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
}
#[test]
fn test_zero_content_width() {
let margins = Margins {
left: 200.0,
right: 200.0,
top: 50.0,
bottom: 50.0,
};
let context = TextFlowContext::new(400.0, 600.0, margins);
assert_eq!(context.content_width(), 0.0);
}
#[test]
fn test_cursor_x_reset_on_newline() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
context.at(250.0, 300.0); context.newline();
assert_eq!(context.cursor_x, margins.left);
assert_eq!(
context.cursor_y,
300.0 - context.font_size * context.line_height
);
}
#[test]
fn test_available_width_respects_cursor_x() {
let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
assert_eq!(context.available_width(), 300.0);
context.at(200.0, 500.0);
assert_eq!(context.available_width(), 150.0);
}
#[test]
fn test_available_width_clamps_to_zero() {
let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.at(380.0, 500.0);
assert_eq!(context.available_width(), 0.0);
}
#[test]
fn test_write_wrapped_at_x_limits_available_width() {
let margins = create_test_margins();
let mut context = TextFlowContext::new(400.0, 600.0, margins);
context.set_font(Font::Helvetica, 12.0);
context.at(250.0, 500.0);
context.write_wrapped("Hello World Hello World").unwrap();
let ops = context.operations();
let bt_count = ops.matches("BT\n").count();
assert!(
bt_count > 1,
"Expected wrapping (multiple lines), got {bt_count} BT blocks. ops:\n{ops}"
);
}
#[test]
fn test_write_wrapped_respects_cursor_x_offset() {
let margins = Margins {
left: 50.0,
right: 50.0,
top: 50.0,
bottom: 50.0,
};
let mut context = TextFlowContext::new(600.0, 800.0, margins);
context.set_font(Font::Helvetica, 12.0);
context.at(300.0, 700.0);
context
.write_wrapped("Hello World Foo Bar Baz Qux")
.unwrap();
let ops = context.operations();
for line in ops.lines() {
if line.ends_with(" Td") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let x: f64 = parts[0].parse().expect("Td x should be a number");
assert!(
x >= 300.0 - 1e-6,
"Expected Td x >= 300.0 but got {x}. ops:\n{ops}"
);
}
}
}
}
#[test]
fn test_text_flow_context_threads_metrics_store() {
use crate::text::metrics::{FontMetrics, FontMetricsStore};
let unique = format!("FlowThreadTask6_{}", std::process::id());
let store = FontMetricsStore::new();
store.register(
unique.clone(),
FontMetrics::new(500).with_widths(&[('A', 1000)]),
);
let mut ctx = TextFlowContext::with_metrics_store(
595.0, 842.0, Margins::default(),
Some(store),
);
ctx.set_font(Font::Custom(unique), 12.0);
ctx.set_alignment(TextAlign::Center);
ctx.write_wrapped("AA").unwrap();
let margins = Margins::default();
let available_width = 595.0_f64 - margins.left - margins.right; let expected_line_width = 24.0_f64; let expected_td_x = margins.left + (available_width - expected_line_width) / 2.0;
let ops_bytes = ctx.generate_operations();
let ops_str =
String::from_utf8(ops_bytes).expect("generated operations must be valid UTF-8");
let td_x: f64 = ops_str
.lines()
.find(|l| l.ends_with(" Td"))
.and_then(|l| l.split_whitespace().next())
.and_then(|tok| tok.parse().ok())
.expect("operations must contain a Td operator");
assert!(
(td_x - expected_td_x).abs() < 0.01,
"Td x must reflect per-store line width 24.0 pts \
(expected {:.2}, got {:.2}); if the store was dropped the \
fallback width produces x ≈ 289.50",
expected_td_x,
td_x
);
}
#[test]
fn nan_cursor_position_in_flow_is_sanitised_at_emission() {
let mut ctx = TextFlowContext::new(595.0, 842.0, Margins::default());
ctx.at(f64::NAN, f64::NAN);
ctx.write_wrapped("hello").unwrap();
let ops = String::from_utf8(ctx.generate_operations())
.expect("operations bytes must be valid UTF-8");
assert!(
!ops.contains("NaN") && !ops.contains("inf"),
"non-finite tokens must not appear in flow content stream, got: {ops:?}"
);
assert!(
ops.contains(" Td\n"),
"Td operator must still be emitted, got: {ops:?}"
);
}
}