Skip to content

Commit f29a6e3

Browse files
authored
Merge pull request #571 from oswald2/multiline_utf8_fix
Multiline text layout panics when multi-byte UTF-8 is used
2 parents 4bdf5d0 + 40c3d67 commit f29a6e3

File tree

2 files changed

+46
-12
lines changed

2 files changed

+46
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
.vscode/*
55
Cargo.lock
66
.idea
7+
plotters/*.svg

plotters/src/element/text.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ impl<'a, Coord, T: Borrow<str>> MultiLineText<'a, Coord, T> {
116116
}
117117
}
118118

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

131-
while !remaining.is_empty() {
132-
let mut left = 0;
133-
while left < remaining.len() {
134-
let width = font.box_size(&remaining[0..=left]).unwrap_or((0, 0)).0 as i32;
137+
let it = std::iter::from_fn(|| {
138+
let start_idx = match indices.next() {
139+
Some(idx) => idx,
140+
None => return None,
141+
};
135142

143+
// iterate over indices
144+
while let Some(idx) = indices.next() {
145+
let substring = &line[start_idx..idx];
146+
let width = font2.box_size(substring).unwrap_or((0, 0)).0 as i32;
136147
if width > max_width as i32 {
137148
break;
138149
}
139-
left += 1;
140150
}
141151

142-
if left == 0 {
143-
left += 1;
144-
}
152+
let end_idx = match indices.peek() {
153+
Some(idx) => *idx,
154+
None => line.bytes().len(),
155+
};
145156

146-
let cur_line = &remaining[..left];
147-
remaining = &remaining[left..];
157+
Some(&line[start_idx..end_idx])
158+
});
148159

149-
func(cur_line);
160+
for chunk in it {
161+
func(chunk);
150162
}
151163
}
152164
}
153165
}
154166

167+
#[cfg(feature = "ttf")]
168+
#[test]
169+
fn test_multi_layout() {
170+
use plotters_backend::{FontFamily, FontStyle};
171+
172+
let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold);
173+
174+
layout_multiline_text("öäabcde", 40, font, |txt| {
175+
println!("Got: {}", txt);
176+
assert!(txt == "öäabc" || txt == "de");
177+
});
178+
179+
let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold);
180+
layout_multiline_text("öä", 100, font, |txt| {
181+
// This does not divide the line, but still crashed in the previous implementation
182+
// of layout_multiline_text. So this test should be reliable
183+
println!("Got: {}", txt);
184+
assert_eq!(txt, "öä")
185+
});
186+
}
187+
155188
impl<'a, T: Borrow<str>> MultiLineText<'a, BackendCoord, T> {
156189
/// Compute the line layout
157190
pub fn compute_line_layout(&self) -> FontResult<Vec<LayoutBox>> {

0 commit comments

Comments
 (0)