use pdfboss_style::Decoration;
use pdfboss_write::{Color, Error, Standard14};
#[derive(Debug, Clone)]
pub(crate) struct StyledRun {
pub text: String,
pub font: Standard14,
pub size: f32,
pub color: Color,
pub background: Option<Color>,
pub decoration: Decoration,
pub link: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct Frag {
pub dx: f32,
pub width: f32,
pub text: String,
pub font: Standard14,
pub size: f32,
pub color: Color,
pub background: Option<Color>,
pub decoration: Decoration,
pub link: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct LineBox {
pub frags: Vec<Frag>,
pub width: f32,
pub max_size: f32,
}
enum Piece<'a> {
Word { run: usize, text: &'a str },
Space { run: usize, text: &'a str },
Break,
}
fn pieces(runs: &[StyledRun]) -> Vec<Piece<'_>> {
let mut out = Vec::new();
for (index, run) in runs.iter().enumerate() {
let text = run.text.as_str();
let mut start = 0;
let mut kind: Option<bool> = None;
for (offset, ch) in text.char_indices() {
if ch == '\n' {
push_segment(&mut out, index, text, start, offset, kind);
out.push(Piece::Break);
start = offset + ch.len_utf8();
kind = None;
continue;
}
let whitespace = ch.is_whitespace();
if kind != Some(whitespace) {
push_segment(&mut out, index, text, start, offset, kind);
start = offset;
kind = Some(whitespace);
}
}
push_segment(&mut out, index, text, start, text.len(), kind);
}
out
}
fn push_segment<'a>(
out: &mut Vec<Piece<'a>>,
run: usize,
text: &'a str,
start: usize,
end: usize,
kind: Option<bool>,
) {
if start >= end {
return;
}
let Some(whitespace) = kind else { return };
let segment = &text[start..end];
if whitespace {
out.push(Piece::Space { run, text: segment });
return;
}
out.push(Piece::Word { run, text: segment });
}
fn measure(run: &StyledRun, text: &str) -> Result<f32, Error> {
run.font.text_width(text, run.size)
}
pub(crate) fn wrap(
runs: &[StyledRun],
max_width: f32,
empty_size: f32,
) -> Result<Vec<LineBox>, Error> {
let mut wrapper = Wrapper::new(runs, max_width, empty_size);
for piece in pieces(runs) {
wrapper.piece(piece)?;
}
wrapper.finish()
}
struct Wrapper<'a> {
runs: &'a [StyledRun],
max_width: f32,
empty_size: f32,
lines: Vec<LineBox>,
line: Vec<Frag>,
line_width: f32,
word: Vec<(usize, String)>,
word_width: f32,
spaces: Vec<(usize, String)>,
space_width: f32,
}
impl<'a> Wrapper<'a> {
fn new(runs: &'a [StyledRun], max_width: f32, empty_size: f32) -> Wrapper<'a> {
Wrapper {
runs,
max_width,
empty_size,
lines: Vec::new(),
line: Vec::new(),
line_width: 0.0,
word: Vec::new(),
word_width: 0.0,
spaces: Vec::new(),
space_width: 0.0,
}
}
fn piece(&mut self, piece: Piece<'a>) -> Result<(), Error> {
match piece {
Piece::Space { run, text } => self.space(run, text),
Piece::Word { run, text } => self.word(run, text),
Piece::Break => self.hard_break(),
}
}
fn space(&mut self, run: usize, text: &str) -> Result<(), Error> {
self.flush_word()?;
let width = measure(&self.runs[run], text)?;
self.spaces.push((run, text.to_string()));
self.space_width += width;
Ok(())
}
fn word(&mut self, run: usize, text: &str) -> Result<(), Error> {
let width = measure(&self.runs[run], text)?;
self.word_width += width;
if let Some((last_run, last_text)) = self.word.last_mut() {
if *last_run == run {
last_text.push_str(text);
return Ok(());
}
}
self.word.push((run, text.to_string()));
Ok(())
}
fn hard_break(&mut self) -> Result<(), Error> {
self.flush_word()?;
self.emit_line();
Ok(())
}
fn flush_word(&mut self) -> Result<(), Error> {
if self.word.is_empty() {
return Ok(());
}
if !self.line.is_empty()
&& self.line_width + self.space_width + self.word_width > self.max_width
{
self.emit_line();
}
if self.line.is_empty() && self.word_width > self.max_width {
self.char_break()?;
}
if !self.line.is_empty() {
self.append_spaces()?;
}
self.spaces.clear();
self.space_width = 0.0;
let word = std::mem::take(&mut self.word);
self.word_width = 0.0;
for (run, text) in word {
self.append_frag(run, text)?;
}
Ok(())
}
fn char_break(&mut self) -> Result<(), Error> {
let word = std::mem::take(&mut self.word);
self.word_width = 0.0;
let mut current: Vec<(usize, String)> = Vec::new();
let mut current_width = 0.0f32;
for (run, text) in word {
for ch in text.chars() {
let mut buffer = [0u8; 4];
let piece = ch.encode_utf8(&mut buffer);
let width = measure(&self.runs[run], piece)?;
if current_width + width > self.max_width && !current.is_empty() {
self.spaces.clear();
self.space_width = 0.0;
for (run, text) in std::mem::take(&mut current) {
self.append_frag(run, text)?;
}
current_width = 0.0;
self.emit_line();
}
match current.last_mut() {
Some((last_run, last_text)) if *last_run == run => last_text.push(ch),
_ => current.push((run, piece.to_string())),
}
current_width += width;
}
}
self.word = current;
self.word_width = current_width;
Ok(())
}
fn append_spaces(&mut self) -> Result<(), Error> {
let spaces = std::mem::take(&mut self.spaces);
for (run, text) in spaces {
let width = measure(&self.runs[run], &text)?;
let dx = self.line_width;
self.push_frag(run, text, dx, width);
}
Ok(())
}
fn append_frag(&mut self, run: usize, text: String) -> Result<(), Error> {
let width = measure(&self.runs[run], &text)?;
let dx = self.line_width;
self.push_frag(run, text, dx, width);
Ok(())
}
fn push_frag(&mut self, run: usize, text: String, dx: f32, width: f32) {
let source = &self.runs[run];
if let Some(last) = self.line.last_mut() {
let same_style = last.font == source.font
&& last.size == source.size
&& last.color == source.color
&& last.background == source.background
&& last.decoration == source.decoration
&& last.link == source.link;
if same_style {
last.text.push_str(&text);
last.width += width;
self.line_width += width;
return;
}
}
self.line.push(Frag {
dx,
width,
text,
font: source.font,
size: source.size,
color: source.color,
background: source.background,
decoration: source.decoration,
link: source.link.clone(),
});
self.line_width += width;
}
fn emit_line(&mut self) {
let frags = std::mem::take(&mut self.line);
let width = self.line_width;
let max_size = frags
.iter()
.map(|frag| frag.size)
.fold(None::<f32>, |acc, size| match acc {
Some(current) => Some(current.max(size)),
None => Some(size),
})
.unwrap_or(self.empty_size);
self.lines.push(LineBox {
frags,
width,
max_size,
});
self.line_width = 0.0;
}
fn finish(mut self) -> Result<Vec<LineBox>, Error> {
self.flush_word()?;
if !self.line.is_empty() {
self.emit_line();
}
Ok(self.lines)
}
}
#[cfg(test)]
mod tests {
use pdfboss_style::Decoration;
use pdfboss_write::{Color, Standard14};
use super::*;
fn mono(text: &str) -> StyledRun {
StyledRun {
text: text.into(),
font: Standard14::Courier,
size: 10.0,
color: Color::BLACK,
background: None,
decoration: Decoration::None,
link: None,
}
}
#[test]
fn wraps_at_word_boundaries() {
let lines = wrap(&[mono("aaa bbb ccc")], 42.0, 10.0).unwrap();
let texts: Vec<String> = lines
.iter()
.map(|l| l.frags.iter().map(|f| f.text.as_str()).collect::<String>())
.collect();
assert_eq!(texts, vec!["aaa bbb", "ccc"]);
assert_eq!(lines[0].width, 42.0);
}
#[test]
fn a_word_spanning_two_runs_stays_together() {
let mut bold = mono("fix");
bold.font = Standard14::CourierBold;
let lines = wrap(&[mono("re"), bold, mono("ed after")], 42.0, 10.0).unwrap();
let first: String = lines[0].frags.iter().map(|f| f.text.as_str()).collect();
assert_eq!(first, "refixed");
assert_eq!(lines[0].frags.len(), 3);
assert_eq!(lines[0].frags[1].dx, 12.0);
}
#[test]
fn hard_break_forces_a_new_line_and_blank_lines_survive() {
let lines = wrap(&[mono("a\n\nb")], 100.0, 10.0).unwrap();
assert_eq!(lines.len(), 3);
assert!(lines[1].frags.is_empty());
assert_eq!(lines[1].max_size, 10.0);
}
#[test]
fn overlong_word_breaks_by_character() {
let lines = wrap(&[mono("abcdefghij")], 30.0, 10.0).unwrap();
let texts: Vec<String> = lines
.iter()
.map(|l| l.frags.iter().map(|f| f.text.as_str()).collect::<String>())
.collect();
assert_eq!(texts, vec!["abcde", "fghij"]);
}
#[test]
fn spaces_at_a_break_are_dropped() {
let lines = wrap(&[mono("aaa bbb")], 20.0, 10.0).unwrap();
assert_eq!(lines[0].width, 18.0);
assert_eq!(lines[1].frags[0].text, "bbb");
assert_eq!(lines[1].frags[0].dx, 0.0);
}
}