use std::iter::Peekable;
use crate::{
LinkTarget,
fonts::{Font, ShapedGlyph},
text::{CacheLinkTarget, pieces::Piece},
};
pub fn lines_from_pieces<'a, F: Font, I: Iterator<Item = (&'a F, &'a Piece)>>(
pieces: I,
max_width: f32,
) -> Lines<'a, F, I> {
Lines {
max_width,
consider_last_line_trailing_whitespace: true,
pieces: PiecesCursor {
iter: pieces.peekable(),
current: None,
},
}
}
pub struct LineGlyph<'a, F> {
pub font: &'a F,
pub text: &'a str,
pub shaped_glyph: ShapedGlyph,
pub size: f32,
pub color: u32,
pub link: Option<LinkTarget<'a>>,
}
pub struct Line<'a, F, P: Iterator<Item = (&'a F, &'a Piece)>> {
pub width: f32,
pub trailing_whitespace_width: f32,
pub height_above_baseline: f32,
pub height_below_baseline: f32,
pieces: std::iter::Take<PiecesCursor<'a, F, P>>,
trailing_hyphen: Option<LineGlyph<'a, F>>,
}
impl<'a, F: Font, P: Iterator<Item = (&'a F, &'a Piece)>> Line<'a, F, P> {
pub fn iter(self) -> impl Iterator<Item = LineGlyph<'a, F>> {
self.pieces
.flat_map(|(main_font, piece)| {
piece.shaped.iter().map(|(font_index, glyph)| LineGlyph {
font: font_index.map_or(main_font, |i| &main_font.fallback_fonts()[i]),
text: &piece.text[glyph.text_range.clone()],
shaped_glyph: glyph.clone(),
size: piece.size,
color: piece.color,
link: piece.link.as_ref().map(CacheLinkTarget::as_link_target),
})
})
.chain(self.trailing_hyphen.into_iter())
}
}
struct PiecesCursor<'a, F, I: Iterator<Item = (&'a F, &'a Piece)>> {
iter: Peekable<I>,
current: Option<(&'a F, &'a Piece)>,
}
impl<'a, F, I: Iterator<Item = (&'a F, &'a Piece)> + Clone> Clone for PiecesCursor<'a, F, I> {
fn clone(&self) -> Self {
Self {
iter: self.iter.clone(),
current: self.current.clone(),
}
}
}
impl<'a, F, I: Iterator<Item = (&'a F, &'a Piece)>> PiecesCursor<'a, F, I> {
fn current(&mut self) -> Option<(&'a F, &'a Piece, bool)> {
if self.current.is_none() {
self.current = self.iter.next();
}
self.current.map(|c| (c.0, c.1, self.iter.peek().is_some()))
}
fn advance(&mut self) {
if self.current.is_some() {
self.current = None;
} else {
self.current = self.iter.next();
}
}
}
impl<'a, F, I: Iterator<Item = (&'a F, &'a Piece)>> Iterator for PiecesCursor<'a, F, I> {
type Item = (&'a F, &'a Piece);
fn next(&mut self) -> Option<Self::Item> {
if self.current.is_some() {
self.current.take()
} else {
self.iter.next()
}
}
}
pub struct Lines<'a, F: Font + 'a, P: Iterator<Item = (&'a F, &'a Piece)>> {
max_width: f32,
consider_last_line_trailing_whitespace: bool,
pieces: PiecesCursor<'a, F, P>,
}
impl<'a, F: Font + 'a, P: Iterator<Item = (&'a F, &'a Piece)> + Clone> Iterator
for Lines<'a, F, P>
{
type Item = Line<'a, F, P>;
fn next(&mut self) -> Option<Line<'a, F, P>> {
if self.pieces.current().is_none() {
return None;
}
let start = self.pieces.clone();
let max_width = self.max_width;
let consider_last_line_trailing_whitespace = self.consider_last_line_trailing_whitespace;
let mut piece_count = 0;
let mut current_width = 0.;
let mut current_width_whitespace = 0.;
let mut trailing_hyphen = None;
let mut height_above_baseline: f32 = 0.;
let mut height_below_baseline: f32 = 0.;
while let Some((font, piece, has_next)) = self.pieces.current() {
if let Some(width) = piece.width
&& current_width > 0.
&& current_width
+ current_width_whitespace
+ width
+ piece
.trailing_hyphen
.as_ref()
.map_or(0., |h| h.1.x_advance * piece.size)
+ (!has_next && consider_last_line_trailing_whitespace)
.then_some(piece.trailing_whitespace_width)
.unwrap_or(0.)
> max_width
{
break;
}
piece_count += 1;
if let Some(width) = piece.width {
current_width += current_width_whitespace + width;
current_width_whitespace = piece.trailing_whitespace_width;
trailing_hyphen = piece.trailing_hyphen.as_ref().map(|x| {
let fallback_fonts = font.fallback_fonts();
LineGlyph {
font: x.0.map_or(font, |i| &fallback_fonts[i]),
text: super::HYPHEN,
shaped_glyph: x.1.clone(),
size: piece.size,
color: piece.color,
link: piece.link.as_ref().map(CacheLinkTarget::as_link_target),
}
});
} else {
current_width_whitespace += piece.trailing_whitespace_width;
}
height_above_baseline = height_above_baseline.max(piece.height_above_baseline);
height_below_baseline = height_below_baseline.max(piece.height_below_baseline);
let mandatory_break_after = piece.mandatory_break_after;
self.pieces.advance();
if mandatory_break_after {
break;
}
}
Some(Line {
width: current_width
+ trailing_hyphen
.as_ref()
.map_or(0., |h| h.shaped_glyph.x_advance * h.size),
trailing_whitespace_width: current_width_whitespace,
height_above_baseline,
height_below_baseline,
pieces: start.take(piece_count),
trailing_hyphen,
})
}
}
#[cfg(test)]
mod tests {
use crate::text::TextPiecesCache;
use super::*;
#[derive(Debug)]
struct FakeFont;
#[derive(Clone, Debug)]
struct FakeShaped<'a> {
inner: std::str::CharIndices<'a>,
}
impl<'a> Iterator for FakeShaped<'a> {
type Item = ShapedGlyph;
fn next(&mut self) -> Option<Self::Item> {
if let Some((i, c)) = self.inner.next() {
Some(ShapedGlyph {
unsafe_to_break: false,
glyph_id: c as u32,
text_range: i..i + c.len_utf8(),
x_advance_font: if matches!(c, '\u{00ad}') { 0. } else { 1. },
x_advance: if matches!(c, '\u{00ad}') { 0. } else { 1. },
x_offset: 0.,
y_offset: 0.,
y_advance: 0.,
})
} else {
None
}
}
}
impl Font for FakeFont {
type Shaped<'a>
= FakeShaped<'a>
where
Self: 'a;
fn shape<'a>(&'a self, text: &'a str, _: f32, _: f32) -> Self::Shaped<'a> {
FakeShaped {
inner: text.char_indices(),
}
}
fn index(&self) -> usize {
0
}
fn encode(&self, _: &mut crate::Pdf, _: u32, _: &str) -> crate::fonts::EncodedGlyph {
unreachable!()
}
fn resource_name(&self) -> pdf_writer::Name<'_> {
unreachable!()
}
fn general_metrics(&self) -> crate::fonts::GeneralMetrics {
crate::fonts::GeneralMetrics {
height_above_baseline: 0.5,
height_below_baseline: 0.5,
}
}
fn fallback_fonts(&self) -> &[Self] {
&[]
}
}
fn lines(text: &str, max_width: f32) -> Vec<(String, f32)> {
let cache = TextPiecesCache::new();
let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
let lines = lines_from_pieces(pieces.iter().map(|p| (&FakeFont, p)), max_width);
lines
.map(|line| {
let width = line.width;
let mut buff = String::new();
for glyph in line.iter() {
let character = glyph.shaped_glyph.glyph_id as u8 as char;
assert_eq!(character.to_string(), glyph.text);
buff.push(character);
}
(buff, width)
})
.collect()
}
#[test]
fn test_empty_string() {
let text = "";
let lines = lines(text, 16.);
assert_eq!(lines, [("".into(), 0.)]);
}
#[test]
fn test_text_flow() {
let text = "Amet consequatur facilis necessitatibus sed quia numquam reiciendis. \
Id impedit quo quaerat enim amet. ";
let lines = lines(text, 16.);
assert_eq!(
lines,
[
("Amet consequatur ".into(), 16.),
("facilis ".into(), 7.),
("necessitatibus ".into(), 14.),
("sed quia numquam ".into(), 16.),
("reiciendis. Id ".into(), 14.),
("impedit quo ".into(), 11.),
("quaerat enim ".into(), 12.),
("amet. ".into(), 5.),
]
);
}
#[test]
fn test_text_after_newline() {
let text = "\nthe the the";
let lines = lines(text, 4.);
assert_eq!(
lines,
[
("".into(), 0.),
("the ".into(), 3.),
("the ".into(), 3.),
("the".into(), 3.),
]
);
}
#[test]
fn test_trailing_whitespace() {
let text = "Id impedit quo quaerat enim amet. ";
let lines = lines(text, 16.);
assert_eq!(
lines,
[
("Id impedit quo ".into(), 14.),
("quaerat enim ".into(), 12.),
("amet. ".into(), 5.),
]
);
}
#[test]
fn test_pre_newline_whitespace() {
let text = "Id impedit quo \nquaerat enimmmmm \namet.";
let lines = lines(text, 16.);
assert_eq!(
lines,
[
("Id impedit quo ".into(), 14.),
("quaerat enimmmmm ".into(), 16.),
("amet.".into(), 5.),
]
)
}
#[test]
fn test_newline() {
let text = "\n";
let lines = lines(text, 16.);
assert_eq!(lines, [("".into(), 0.), ("".into(), 0.)]);
}
#[test]
fn test_just_spaces() {
let text = " ";
let lines = lines(text, 16.);
assert_eq!(lines, [(" ".into(), 0.)]);
}
#[test]
fn test_word_longer_than_line() {
let text = "Averylongword";
assert_eq!(lines(text, 8.), [("Averylongword".into(), 13.)]);
let text = "Averylongword test.";
assert_eq!(
lines(text, 8.),
[("Averylongword ".into(), 13.), ("test.".into(), 5.)]
);
let text = "A verylongword test.";
assert_eq!(
lines(text, 8.),
[
("A ".into(), 1.),
("verylongword ".into(), 12.),
("test.".into(), 5.),
],
);
}
#[test]
fn test_soft_hyphens() {
let text = "A\u{00ad}very\u{00ad}long\u{00ad}word";
assert_eq!(
lines(text, 7.),
[
("A\u{00ad}very\u{00ad}-".into(), 6.),
("long\u{00ad}-".into(), 5.),
("word".into(), 4.),
],
);
let text = "A\u{00ad}very \u{00ad}long\u{00ad}word";
assert_eq!(
lines(text, 7.),
[
("A\u{00ad}very \u{00ad}-".into(), 7.),
("long\u{00ad}-".into(), 5.),
("word".into(), 4.),
],
);
let text = "A\u{00ad}very\u{00ad}\u{00ad}long\u{00ad}word";
assert_eq!(
lines(text, 7.),
[
("A\u{00ad}very\u{00ad}\u{00ad}-".into(), 6.),
("long\u{00ad}-".into(), 5.),
("word".into(), 4.),
],
);
}
#[test]
fn test_hard_hyphens() {
let text = "A-very-long-word";
assert_eq!(
lines(text, 7.),
[
("A-very-".into(), 7.),
("long-".into(), 5.),
("word".into(), 4.),
],
);
let text = "A-very -long-word";
assert_eq!(
lines(text, 7.),
[
("A-very ".into(), 6.),
("-long-".into(), 6.),
("word".into(), 4.),
],
);
let text = "A-very--long-word";
assert_eq!(
lines(text, 7.),
[
("A-".into(), 2.),
("very--".into(), 6.),
("long-".into(), 5.),
("word".into(), 4.),
],
);
}
}