Skip to main content

oxitext_shape/
batch.rs

1//! Batch shaping: shape multiple text segments in a single call.
2//!
3//! Provides [`SwashShaper::shape_batch`] and
4//! [`SwashShaper::shape_batch_directed`] for amortised setup across many
5//! independent text segments sharing the same font and size.
6
7use crate::{ShapeDirection, ShapeFeature, ShapeRequest, ShapeResult, SwashShaper};
8use oxitext_core::OxiTextError;
9
10impl SwashShaper {
11    /// Shape multiple independent text segments with the same font and size.
12    ///
13    /// Returns one [`ShapeResult`] per input segment.
14    /// More efficient than calling [`SwashShaper::shape_full`] N times for the
15    /// same `(font_data, px_size)` pair because the [`ShapeContext`] is reused
16    /// across all calls without reallocation.
17    ///
18    /// # Errors
19    /// Each element of the returned `Vec` is `Ok` when the font can be parsed
20    /// and `Err` when shaping fails for that segment.
21    ///
22    /// [`ShapeContext`]: swash::shape::ShapeContext
23    pub fn shape_batch(
24        &mut self,
25        font_data: &[u8],
26        segments: &[&str],
27        px_size: f32,
28    ) -> Vec<Result<ShapeResult, OxiTextError>> {
29        segments
30            .iter()
31            .map(|text| self.shape_full(font_data, text, px_size))
32            .collect()
33    }
34
35    /// Shape a batch with per-segment directions.
36    ///
37    /// Each element of `segments` is a `(text, direction)` pair.  The
38    /// appropriate shaping direction is applied to each segment independently.
39    /// Vertical directions (`Ttb`, `Btt`) automatically inject `vert`/`vrt2`
40    /// features, matching the behaviour of [`SwashShaper::shape_request`].
41    ///
42    /// Returns one [`ShapeResult`] per input pair.
43    pub fn shape_batch_directed(
44        &mut self,
45        font_data: &[u8],
46        segments: &[(&str, ShapeDirection)],
47        px_size: f32,
48    ) -> Vec<Result<ShapeResult, OxiTextError>> {
49        segments
50            .iter()
51            .map(|(text, dir)| {
52                let req = ShapeRequest::builder()
53                    .text(text)
54                    .font_data(font_data)
55                    .px_size(px_size)
56                    .direction(*dir)
57                    .build()
58                    .map_err(|e| OxiTextError::Shaping(e.to_string()))?;
59                let glyphs = self.shape_request(&req)?;
60                Ok(ShapeResult::from_glyphs(glyphs, text, *dir))
61            })
62            .collect()
63    }
64
65    /// Shape a batch with a shared feature list applied to every segment.
66    ///
67    /// All segments share the same `(font_data, px_size, features)` context.
68    ///
69    /// Returns one [`ShapeResult`] per input segment.
70    pub fn shape_batch_with_features(
71        &mut self,
72        font_data: &[u8],
73        segments: &[&str],
74        px_size: f32,
75        features: &[ShapeFeature],
76    ) -> Vec<Result<ShapeResult, OxiTextError>> {
77        segments
78            .iter()
79            .map(|text| {
80                let glyphs = self.shape_with_features(font_data, text, px_size, false, features)?;
81                Ok(ShapeResult::from_glyphs(glyphs, text, ShapeDirection::Ltr))
82            })
83            .collect()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::path::Path;
91    use std::sync::Arc;
92
93    fn load_test_font() -> Arc<[u8]> {
94        let fixture =
95            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/test-font.ttf");
96        if fixture.exists() {
97            return Arc::from(
98                std::fs::read(&fixture)
99                    .expect("read fixture font")
100                    .as_slice(),
101            );
102        }
103        let candidates = [
104            "/Library/Fonts/Arial Unicode.ttf",
105            "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
106        ];
107        for p in &candidates {
108            if Path::new(p).exists() {
109                return Arc::from(std::fs::read(p).expect("read system font").as_slice());
110            }
111        }
112        panic!("no test font found — add tests/fixtures/test-font.ttf");
113    }
114
115    #[test]
116    fn test_shape_batch_produces_one_result_per_segment() {
117        let font_bytes = load_test_font();
118        let mut shaper = SwashShaper::new();
119        let segments = ["Hello", "World", "Test"];
120        let results = shaper.shape_batch(&font_bytes, &segments, 16.0);
121        assert_eq!(results.len(), 3);
122        for r in &results {
123            assert!(r.is_ok(), "expected Ok for segment, got: {r:?}");
124        }
125    }
126
127    #[test]
128    fn test_shape_batch_empty_segments() {
129        let font_bytes = load_test_font();
130        let mut shaper = SwashShaper::new();
131        let results = shaper.shape_batch(&font_bytes, &[], 16.0);
132        assert!(results.is_empty());
133    }
134
135    #[test]
136    fn test_shape_batch_directed_mixed() {
137        let font_bytes = load_test_font();
138        let mut shaper = SwashShaper::new();
139        let segments = [
140            ("Hello", ShapeDirection::Ltr),
141            ("world", ShapeDirection::Ltr),
142        ];
143        let results = shaper.shape_batch_directed(&font_bytes, &segments, 16.0);
144        assert_eq!(results.len(), 2);
145        for r in &results {
146            assert!(
147                r.is_ok(),
148                "expected Ok from shape_batch_directed, got: {r:?}"
149            );
150        }
151    }
152
153    #[test]
154    fn test_shape_batch_glyphs_are_non_empty() {
155        let font_bytes = load_test_font();
156        let mut shaper = SwashShaper::new();
157        let segments = ["Hi", "Test"];
158        let results = shaper.shape_batch(&font_bytes, &segments, 16.0);
159        for r in &results {
160            let shape = r.as_ref().expect("batch result ok");
161            assert!(
162                !shape.glyphs.is_empty(),
163                "expected non-empty glyphs per segment"
164            );
165        }
166    }
167}