Skip to main content

oxitext_shape/
backend.rs

1//! Swappable text shaping backends.
2//!
3//! Defines the [`ShapeBackend`] trait so consumers can choose between the
4//! default swash-based backend and the optional rustybuzz backend without
5//! changing any other part of the pipeline.
6
7use oxitext_core::ShapedGlyph;
8use std::sync::Arc;
9
10/// Trait for swappable text shaping backends.
11///
12/// Implementors must be `Send + Sync` so they can be shared across threads.
13///
14/// All methods receive `face_data: &Arc<[u8]>` rather than `&[u8]` so that the
15/// pointer address is preserved across calls.  This allows [`crate::ShapeCache`]
16/// (keyed on `Arc::as_ptr`) to produce cache hits when the same allocation is
17/// reused.
18pub trait ShapeBackend: Send + Sync {
19    /// Shape UTF-8 `text` using font bytes `face_data` at `px_size`
20    /// pixels-per-em, returning one [`ShapedGlyph`] per output glyph.
21    fn shape(&self, face_data: &Arc<[u8]>, text: &str, px_size: f32) -> Vec<ShapedGlyph>;
22
23    /// Shape UTF-8 `text` with an explicit direction hint.
24    ///
25    /// When `rtl` is `false` this is identical to [`Self::shape`].
26    ///
27    /// When `rtl` is `true` the backend shapes in the right-to-left direction
28    /// and returns glyphs sorted in **ascending `cluster` order** (logical
29    /// source order).  Visual reordering is the caller's responsibility.
30    ///
31    /// Backends that do not override this method fall back to [`Self::shape`]
32    /// for LTR and perform a post-sort for RTL, which is correct but may not
33    /// select the best glyph forms for bidirectional scripts.
34    fn shape_with_direction(
35        &self,
36        face_data: &Arc<[u8]>,
37        text: &str,
38        px_size: f32,
39        rtl: bool,
40    ) -> Vec<ShapedGlyph> {
41        if !rtl {
42            return self.shape(face_data, text, px_size);
43        }
44        let mut glyphs = self.shape(face_data, text, px_size);
45        glyphs.sort_by_key(|g| g.cluster);
46        glyphs
47    }
48
49    /// Shape UTF-8 `text` with an explicit set of OpenType feature overrides.
50    ///
51    /// The default implementation ignores the `features` slice and delegates
52    /// to [`Self::shape`].  Backends that support OpenType feature control
53    /// should override this method to apply the requested features.
54    fn shape_with_features(
55        &self,
56        face_data: &Arc<[u8]>,
57        text: &str,
58        px_size: f32,
59        features: &[crate::ShapeFeature],
60    ) -> Vec<ShapedGlyph> {
61        let _ = features;
62        self.shape(face_data, text, px_size)
63    }
64
65    /// Shape text with extended options.
66    ///
67    /// The default implementation delegates to [`Self::shape_with_features`]
68    /// when `features` is non-empty, or [`Self::shape`] otherwise, ignoring
69    /// any options not covered by those methods.  Backends that need to honour
70    /// additional fields from `options` (script, language, direction, etc.)
71    /// should override this method.
72    fn shape_with_options(
73        &self,
74        face_data: &Arc<[u8]>,
75        text: &str,
76        px_size: f32,
77        rtl: bool,
78        features: &[crate::ShapeFeature],
79        _options: &crate::ShapeRequest<'_>,
80    ) -> Vec<ShapedGlyph> {
81        if features.is_empty() {
82            self.shape_with_direction(face_data, text, px_size, rtl)
83        } else {
84            let mut glyphs = self.shape_with_features(face_data, text, px_size, features);
85            if rtl {
86                glyphs.sort_by_key(|g| g.cluster);
87            }
88            glyphs
89        }
90    }
91
92    /// Check if the font has shaping support for a given OpenType script tag.
93    ///
94    /// Returns `true` if the font likely covers the script, `false` if a
95    /// sentinel character from the script's Unicode range returns no glyph.
96    /// Unknown scripts (not in the built-in table) return `true` by default
97    /// so callers that do not pass script tags are not affected.
98    fn supports_script(&self, font_data: &Arc<[u8]>, script: [u8; 4]) -> bool {
99        /// Sentinel characters to check per well-known script tag.
100        fn sentinel_char(script: [u8; 4]) -> Option<char> {
101            match &script {
102                b"latn" => Some('A'),
103                b"arab" => Some('\u{0627}'), // Arabic letter Alef
104                b"hani" => Some('\u{4E00}'), // CJK ideograph
105                b"cyrl" => Some('\u{0410}'), // Cyrillic capital A
106                b"grek" => Some('\u{0391}'), // Greek capital Alpha
107                b"hebr" => Some('\u{05D0}'), // Hebrew Alef
108                b"deva" => Some('\u{0905}'), // Devanagari A
109                b"thai" => Some('\u{0E01}'), // Thai Ko Kai
110                _ => None,
111            }
112        }
113
114        let Some(ch) = sentinel_char(script) else {
115            // Unknown script — permissive default.
116            return true;
117        };
118
119        ttf_parser::Face::parse(font_data.as_ref(), 0)
120            .map(|face| face.glyph_index(ch).is_some())
121            .unwrap_or(true) // If parsing fails, assume support.
122    }
123}
124
125/// Swash-based shaper — delegates to [`crate::SwashShaper`].
126///
127/// This adapter wraps the existing [`crate::SwashShaper`] (which has its own
128/// internal LRU cache) and exposes it via the [`ShapeBackend`] trait.
129///
130/// Uses [`std::sync::RwLock`] instead of `Mutex` so that read-only operations
131/// (e.g. [`ShapeBackend::supports_script`]) can proceed concurrently.  Shape
132/// calls still acquire the write lock because the underlying [`crate::SwashShaper`]
133/// mutates its internal context on every call.
134pub struct SwashShaperBackend {
135    inner: std::sync::Arc<std::sync::RwLock<crate::SwashShaper>>,
136}
137
138impl SwashShaperBackend {
139    /// Creates a new [`SwashShaperBackend`].
140    pub fn new() -> Self {
141        Self {
142            inner: std::sync::Arc::new(std::sync::RwLock::new(crate::SwashShaper::new())),
143        }
144    }
145}
146
147impl Default for SwashShaperBackend {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl ShapeBackend for SwashShaperBackend {
154    fn shape(&self, face_data: &Arc<[u8]>, text: &str, px_size: f32) -> Vec<ShapedGlyph> {
155        let mut guard = match self.inner.write() {
156            Ok(g) => g,
157            Err(_) => return Vec::new(),
158        };
159        match guard.shape(text, Arc::clone(face_data), px_size) {
160            Ok(run) => run.glyphs.into_vec(),
161            Err(_) => Vec::new(),
162        }
163    }
164
165    fn shape_with_direction(
166        &self,
167        face_data: &Arc<[u8]>,
168        text: &str,
169        px_size: f32,
170        rtl: bool,
171    ) -> Vec<ShapedGlyph> {
172        let mut guard = match self.inner.write() {
173            Ok(g) => g,
174            Err(_) => return Vec::new(),
175        };
176        match guard.shape_with_direction(text, Arc::clone(face_data), px_size, rtl) {
177            Ok(run) => run.glyphs.into_vec(),
178            Err(_) => Vec::new(),
179        }
180    }
181}
182
183/// rustybuzz-based shaper.
184///
185/// Enabled by the `rustybuzz-backend` feature.
186#[cfg(feature = "rustybuzz-backend")]
187pub struct RustybuzzShaper;
188
189#[cfg(feature = "rustybuzz-backend")]
190impl Default for RustybuzzShaper {
191    fn default() -> Self {
192        Self
193    }
194}
195
196#[cfg(feature = "rustybuzz-backend")]
197impl RustybuzzShaper {
198    /// Internal helper: shapes `text` into glyphs using the given rustybuzz direction.
199    ///
200    /// Returns glyphs sorted to ascending cluster (logical source) order.
201    fn shape_internal(
202        &self,
203        face_data: &Arc<[u8]>,
204        text: &str,
205        px_size: f32,
206        direction: rustybuzz::Direction,
207    ) -> Vec<ShapedGlyph> {
208        use rustybuzz::{Face, UnicodeBuffer};
209
210        let face = match Face::from_slice(face_data.as_ref(), 0) {
211            Some(f) => f,
212            None => return Vec::new(),
213        };
214
215        let upem = face.units_per_em() as f32;
216        let scale = if upem > 0.0 { px_size / upem } else { 1.0 };
217
218        let mut buf = UnicodeBuffer::new();
219        buf.push_str(text);
220        buf.set_direction(direction);
221
222        let shaped = rustybuzz::shape(&face, &[], buf);
223        let infos = shaped.glyph_infos();
224        let positions = shaped.glyph_positions();
225
226        let mut glyphs: Vec<ShapedGlyph> = infos
227            .iter()
228            .zip(positions.iter())
229            .map(|(info, pos)| {
230                let is_ws = text
231                    .get(info.cluster as usize..)
232                    .and_then(|s| s.chars().next())
233                    .map(|c| c.is_whitespace())
234                    .unwrap_or(false);
235                ShapedGlyph {
236                    gid: info.glyph_id as u16,
237                    cluster: info.cluster,
238                    x_advance: pos.x_advance as f32 * scale,
239                    y_advance: pos.y_advance as f32 * scale,
240                    x_offset: pos.x_offset as f32 * scale,
241                    y_offset: pos.y_offset as f32 * scale,
242                    is_whitespace: is_ws,
243                    // rustybuzz exposes UNSAFE_TO_BREAK in glyph flags; we
244                    // conservatively leave it false here (single-pass shaping).
245                    unsafe_to_break: false,
246                }
247            })
248            .collect();
249
250        // Guarantee ascending cluster (logical source) order regardless of
251        // what rustybuzz emits for RTL runs.
252        glyphs.sort_by_key(|g| g.cluster);
253        glyphs
254    }
255}
256
257#[cfg(feature = "rustybuzz-backend")]
258impl ShapeBackend for RustybuzzShaper {
259    fn shape(&self, face_data: &Arc<[u8]>, text: &str, px_size: f32) -> Vec<ShapedGlyph> {
260        self.shape_internal(face_data, text, px_size, rustybuzz::Direction::LeftToRight)
261    }
262
263    fn shape_with_direction(
264        &self,
265        face_data: &Arc<[u8]>,
266        text: &str,
267        px_size: f32,
268        rtl: bool,
269    ) -> Vec<ShapedGlyph> {
270        let direction = if rtl {
271            rustybuzz::Direction::RightToLeft
272        } else {
273            rustybuzz::Direction::LeftToRight
274        };
275        self.shape_internal(face_data, text, px_size, direction)
276    }
277}