oxitext_shape/
variational.rs1use crate::{ShapeResult, SwashShaper};
8use oxitext_core::OxiTextError;
9
10impl SwashShaper {
11 pub fn shape_with_variations(
29 &mut self,
30 font_data: &[u8],
31 text: &str,
32 px_size: f32,
33 variations: &[([u8; 4], f32)],
34 ) -> Result<ShapeResult, OxiTextError> {
35 let _ = variations;
38 self.shape_full(font_data, text, px_size)
39 }
40}
41
42#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::{ShapeDirection, ShapeFeature, ShapeRequest};
50 use std::path::Path;
51 use std::sync::Arc;
52
53 fn load_test_font() -> Arc<[u8]> {
54 let fixture =
55 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/test-font.ttf");
56 if fixture.exists() {
57 return Arc::from(
58 std::fs::read(&fixture)
59 .expect("read fixture font")
60 .as_slice(),
61 );
62 }
63 let candidates = [
64 "/Library/Fonts/Arial Unicode.ttf",
65 "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
66 ];
67 for p in &candidates {
68 if Path::new(p).exists() {
69 return Arc::from(std::fs::read(p).expect("read system font").as_slice());
70 }
71 }
72 panic!("no test font found — add tests/fixtures/test-font.ttf");
73 }
74
75 #[test]
78 fn test_vertical_shaping_applies_vert_feature() {
79 let font_bytes = load_test_font();
80 let mut shaper = SwashShaper::new();
81 let req = ShapeRequest::builder()
83 .text("A")
84 .font_data(&font_bytes)
85 .px_size(16.0)
86 .direction(ShapeDirection::Ttb)
87 .build()
88 .expect("build request");
89 let result = shaper.shape_request(&req);
91 assert!(
92 result.is_ok(),
93 "shape_request with Ttb should succeed: {result:?}"
94 );
95 let glyphs = result.expect("ok");
96 assert!(
97 !glyphs.is_empty(),
98 "expected at least one glyph from vertical shaping"
99 );
100 }
101
102 #[test]
103 fn test_vertical_shaping_shape_with_features_vert() {
104 let font_bytes = load_test_font();
106 let mut shaper = SwashShaper::new();
107 let vert = ShapeFeature::VERT;
108 let result = shaper.shape_with_features(&font_bytes, "A", 16.0, false, &[vert]);
109 assert!(
110 result.is_ok(),
111 "shape_with_features with VERT should succeed: {result:?}"
112 );
113 }
114
115 #[test]
116 fn test_vertical_shaping_vert_and_vrt2_injected() {
117 let req = ShapeRequest::builder()
119 .text("A")
120 .font_data(&[0u8; 4])
121 .px_size(16.0)
122 .direction(ShapeDirection::Ttb)
123 .build()
124 .expect("build ok");
125 let mut features = req.features.clone();
126 if req.direction == ShapeDirection::Ttb {
127 if !features.iter().any(|f| f.tag == *b"vert") {
128 features.push(ShapeFeature::VERT);
129 }
130 if !features.iter().any(|f| f.tag == *b"vrt2") {
131 features.push(ShapeFeature::VRT2);
132 }
133 }
134 assert!(
135 features.iter().any(|f| f.tag == *b"vert"),
136 "vert must be present for Ttb"
137 );
138 assert!(
139 features.iter().any(|f| f.tag == *b"vrt2"),
140 "vrt2 must be present for Ttb"
141 );
142 }
143
144 #[test]
147 fn test_shape_with_variations_does_not_panic() {
148 let font_bytes = load_test_font();
149 let mut shaper = SwashShaper::new();
150 let variations = [(*b"wght", 700.0f32)];
151 let result = shaper.shape_with_variations(&font_bytes, "Hello", 16.0, &variations);
152 assert!(
154 result.is_ok(),
155 "shape_with_variations should succeed: {result:?}"
156 );
157 }
158
159 #[test]
160 fn test_shape_with_variations_empty_variations() {
161 let font_bytes = load_test_font();
162 let mut shaper = SwashShaper::new();
163 let result = shaper.shape_with_variations(&font_bytes, "ABC", 16.0, &[]);
164 assert!(result.is_ok(), "empty variations list must succeed");
165 let r = result.expect("ok");
166 assert!(!r.glyphs.is_empty(), "expected shaped glyphs");
167 }
168
169 #[test]
170 fn test_shape_with_variations_multiple_axes() {
171 let font_bytes = load_test_font();
172 let mut shaper = SwashShaper::new();
173 let variations = [(*b"wght", 400.0f32), (*b"wdth", 100.0f32)];
174 let result = shaper.shape_with_variations(&font_bytes, "Hi", 16.0, &variations);
175 assert!(
176 result.is_ok(),
177 "multiple variation axes must not error: {result:?}"
178 );
179 }
180
181 #[test]
188 fn test_devanagari_requires_indic_shaping() {
189 assert!(
190 crate::requires_indic_shaping("क्ष"),
191 "ksha (ka + virama + sha) must require Indic shaping"
192 );
193 assert!(
194 crate::requires_indic_shaping("नमस्ते"),
195 "namaste must require Indic shaping"
196 );
197 assert!(
198 !crate::requires_indic_shaping("Hello"),
199 "Latin text must not require Indic shaping"
200 );
201 }
202
203 #[test]
204 fn test_devanagari_virama_text_requires_indic_shaping() {
205 let text = "क\u{094D}ष";
207 assert!(
208 crate::requires_indic_shaping(text),
209 "explicit virama sequence must require Indic shaping"
210 );
211 }
212
213 #[test]
214 fn test_devanagari_conjunct_swash_no_panic() {
215 let devanagari_ksha = "क्ष";
219 let font_bytes = load_test_font();
220 let mut shaper = SwashShaper::new();
221 let result = shaper.shape_full(&font_bytes, devanagari_ksha, 16.0);
222 match result {
224 Ok(sr) => {
225 assert!(
227 !sr.cluster_boundaries.is_empty(),
228 "shape_full must populate cluster_boundaries"
229 );
230 }
231 Err(_) => {
232 }
234 }
235 }
236
237 #[cfg(feature = "rustybuzz-backend")]
238 #[test]
239 fn test_devanagari_conjunct_rustybuzz_no_panic() {
240 use crate::backend::ShapeBackend as _;
241
242 let devanagari_ksha = "क्ष";
243 let font_bytes = load_test_font();
244 let backend = crate::RustybuzzShaper;
247 let glyphs = backend.shape(&font_bytes, devanagari_ksha, 16.0);
248 let _ = glyphs;
251 }
252
253 #[test]
256 fn test_shape_cache_correctness_repeated_font() {
257 let font_bytes = load_test_font();
258 let mut shaper = SwashShaper::with_cache(64);
259
260 let results: Vec<_> = (0..5)
262 .map(|_| shaper.shape("Hello", Arc::clone(&font_bytes), 16.0).ok())
263 .collect();
264
265 let first = &results[0];
266 for r in &results[1..] {
267 match (first, r) {
268 (Some(a), Some(b)) => {
269 assert_eq!(
270 a.glyphs.len(),
271 b.glyphs.len(),
272 "repeated shape call must return the same glyph count"
273 );
274 for (ga, gb) in a.glyphs.iter().zip(b.glyphs.iter()) {
275 assert_eq!(ga.gid, gb.gid, "cached glyph IDs must be stable");
276 }
277 }
278 (None, None) => {}
279 _ => panic!(
280 "inconsistent cache behaviour: first was Some={}, subsequent was Some={}",
281 first.is_some(),
282 r.is_some()
283 ),
284 }
285 }
286 }
287
288 #[test]
289 fn test_shape_cache_correctness_different_texts() {
290 let font_bytes = load_test_font();
291 let mut shaper = SwashShaper::with_cache(64);
292
293 let r1 = shaper.shape("AAA", Arc::clone(&font_bytes), 16.0).ok();
294 let r2 = shaper.shape("BBB", Arc::clone(&font_bytes), 16.0).ok();
295 let r3 = shaper.shape("AAA", Arc::clone(&font_bytes), 16.0).ok();
296
297 if let (Some(a), Some(b)) = (&r1, &r3) {
299 assert_eq!(
300 a.glyphs.len(),
301 b.glyphs.len(),
302 "same text shaped twice must yield the same glyph count"
303 );
304 }
305 let _ = r2;
308 }
309
310 #[test]
313 fn test_font_has_aat_is_idempotent() {
314 let font_bytes = load_test_font();
318 let r1 = SwashShaper::font_has_aat(&font_bytes);
319 let r2 = SwashShaper::font_has_aat(&font_bytes);
320 assert_eq!(
321 r1, r2,
322 "font_has_aat must return the same value on repeated calls"
323 );
324 }
325}