Skip to content

Commit

Permalink
Merge pull request #571 from oswald2/multiline_utf8_fix
Browse files Browse the repository at this point in the history
Multiline text layout panics when multi-byte UTF-8 is used
  • Loading branch information
AaronErhardt committed May 20, 2024
2 parents 4bdf5d0 + 40c3d67 commit f29a6e3
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.vscode/*
Cargo.lock
.idea
plotters/*.svg
57 changes: 45 additions & 12 deletions plotters/src/element/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ impl<'a, Coord, T: Borrow<str>> MultiLineText<'a, Coord, T> {
}
}


// Rewrite of the layout function for multiline-text. It crashes when UTF-8 is used
// instead of ASCII. Solution taken from:
// https://stackoverflow.com/questions/68122526/splitting-a-utf-8-string-into-chunks
// and modified for our purposes.
fn layout_multiline_text<'a, F: FnMut(&'a str)>(
text: &'a str,
max_width: u32,
Expand All @@ -126,32 +131,60 @@ fn layout_multiline_text<'a, F: FnMut(&'a str)>(
if max_width == 0 || line.is_empty() {
func(line);
} else {
let mut remaining = &line[0..];
let mut indices = line.char_indices().map(|(idx, _)| idx).peekable();
let font2 = font.clone();

while !remaining.is_empty() {
let mut left = 0;
while left < remaining.len() {
let width = font.box_size(&remaining[0..=left]).unwrap_or((0, 0)).0 as i32;
let it = std::iter::from_fn(|| {
let start_idx = match indices.next() {
Some(idx) => idx,
None => return None,
};

// iterate over indices
while let Some(idx) = indices.next() {
let substring = &line[start_idx..idx];
let width = font2.box_size(substring).unwrap_or((0, 0)).0 as i32;
if width > max_width as i32 {
break;
}
left += 1;
}

if left == 0 {
left += 1;
}
let end_idx = match indices.peek() {
Some(idx) => *idx,
None => line.bytes().len(),
};

let cur_line = &remaining[..left];
remaining = &remaining[left..];
Some(&line[start_idx..end_idx])
});

func(cur_line);
for chunk in it {
func(chunk);
}
}
}
}

#[cfg(feature = "ttf")]
#[test]
fn test_multi_layout() {
use plotters_backend::{FontFamily, FontStyle};

let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold);

layout_multiline_text("öäabcde", 40, font, |txt| {
println!("Got: {}", txt);
assert!(txt == "öäabc" || txt == "de");
});

let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold);
layout_multiline_text("öä", 100, font, |txt| {
// This does not divide the line, but still crashed in the previous implementation
// of layout_multiline_text. So this test should be reliable
println!("Got: {}", txt);
assert_eq!(txt, "öä")
});
}

impl<'a, T: Borrow<str>> MultiLineText<'a, BackendCoord, T> {
/// Compute the line layout
pub fn compute_line_layout(&self) -> FontResult<Vec<LayoutBox>> {
Expand Down

0 comments on commit f29a6e3

Please sign in to comment.