Skip to main content

typf_core/
pipeline.rs

1//! The engine that drives text through six stages to become images
2
3// this_file: crates/typf-core/src/pipeline.rs
4
5use crate::{
6    context::PipelineContext,
7    error::{Result, TypfError},
8    glyph_cache::{GlyphCache, GlyphCacheKey, SharedGlyphCache},
9    shaping_cache::{ShapingCache, ShapingCacheKey, SharedShapingCache},
10    traits::{Exporter, FontRef, Renderer, Shaper, Stage},
11    RenderParams, ShapingParams,
12};
13use std::sync::{Arc, RwLock};
14
15/// Pipeline for text rendering: Shape → Render → Export
16///
17/// The Pipeline provides two methods for processing text:
18///
19/// ## `process()` - Direct Backend Execution (Recommended)
20/// Directly chains the three core backends:
21/// - **Shaping** - Characters transform into positioned glyphs
22/// - **Rendering** - Glyphs become pixels or vectors
23/// - **Export** - Final output emerges as files
24///
25/// ## `execute()` - Stage-Based Execution
26/// Runs through six stages, with the first three being pass-through:
27/// 1. InputParsing - (pass-through, reserved for future use)
28/// 2. UnicodeProcessing - (pass-through, reserved for future use)
29/// 3. FontSelection - (pass-through, reserved for future use)
30/// 4. **Shaping** - Characters transform into positioned glyphs
31/// 5. **Rendering** - Glyphs become pixels or vectors
32/// 6. **Export** - Final output emerges as files
33///
34/// For most use cases, prefer `process()` which is simpler and avoids the
35/// pass-through stage overhead.
36///
37/// ```ignore
38/// use typf_core::Pipeline;
39///
40/// let pipeline = Pipeline::builder()
41///     .shaper(my_shaper)
42///     .renderer(my_renderer)
43///     .exporter(my_exporter)
44///     .build()?;
45///
46/// let result = pipeline.process(
47///     "Hello, world!",
48///     font,
49///     &shaping_params,
50///     &render_params,
51/// )?;
52/// ```
53pub struct Pipeline {
54    stages: Vec<Box<dyn Stage>>,
55    shaper: Option<Arc<dyn Shaper>>,
56    renderer: Option<Arc<dyn Renderer>>,
57    exporter: Option<Arc<dyn Exporter>>,
58    #[allow(dead_code)]
59    cache_policy: CachePolicy,
60    #[allow(dead_code)]
61    shaping_cache: Option<SharedShapingCache>,
62    #[allow(dead_code)]
63    glyph_cache: Option<SharedGlyphCache>,
64}
65
66/// Controls runtime caching behaviour for pipelines.
67///
68/// **Note:** Caching is disabled by default. Enable it via:
69/// - `PipelineBuilder::enable_shaping_cache(true)` and/or
70/// - `PipelineBuilder::enable_glyph_cache(true)`
71/// - Or globally via `cache_config::set_caching_enabled(true)`
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
73pub struct CachePolicy {
74    pub shaping: bool,
75    pub glyph: bool,
76}
77
78impl Pipeline {
79    /// Start building a new pipeline
80    pub fn builder() -> PipelineBuilder {
81        PipelineBuilder::new()
82    }
83
84    /// Send text through all six stages and get the final bytes
85    pub fn process(
86        &self,
87        text: &str,
88        font: Arc<dyn FontRef>,
89        shaping_params: &ShapingParams,
90        render_params: &RenderParams,
91    ) -> Result<Vec<u8>> {
92        // Grab the three essential backends
93        let shaper = self
94            .shaper
95            .as_ref()
96            .ok_or_else(|| TypfError::ConfigError("No shaper configured".into()))?;
97        let renderer = self
98            .renderer
99            .as_ref()
100            .ok_or_else(|| TypfError::ConfigError("No renderer configured".into()))?;
101        let exporter = self
102            .exporter
103            .as_ref()
104            .ok_or_else(|| TypfError::ConfigError("No exporter configured".into()))?;
105
106        // Shape → Render → Export
107        let shaped = shaper.shape(text, font.clone(), shaping_params)?;
108        let rendered = renderer.render(&shaped, font, render_params)?;
109        let exported = exporter.export(&rendered)?;
110
111        Ok(exported)
112    }
113
114    /// Run the full pipeline with a prepared context
115    pub fn execute(&self, mut context: PipelineContext) -> Result<PipelineContext> {
116        // Backends need to know where they live
117        if let Some(shaper) = &self.shaper {
118            context.set_shaper(shaper.clone());
119        }
120        if let Some(renderer) = &self.renderer {
121            context.set_renderer(renderer.clone());
122        }
123        if let Some(exporter) = &self.exporter {
124            context.set_exporter(exporter.clone());
125        }
126
127        // One stage at a time, each transforms the context
128        for stage in &self.stages {
129            log::debug!("Executing stage: {}", stage.name());
130            context = stage.process(context)?;
131        }
132
133        Ok(context)
134    }
135}
136
137/// Build pipelines your way, piece by piece
138///
139/// Chain together the backends you need, customize stages, or use defaults.
140/// The builder pattern makes configuration clear and explicit.
141///
142/// ```ignore
143/// use typf_core::Pipeline;
144///
145/// // Quick start with defaults
146/// let pipeline = Pipeline::builder()
147///     .shaper(Arc::new(HarfBuzzShaper::new()))
148///     .renderer(Arc::new(OpixaRenderer::new()))
149///     .exporter(Arc::new(PnmExporter::new(PnmFormat::Ppm)))
150///     .build()?;
151///
152/// // Full control with custom stages
153/// let pipeline = Pipeline::builder()
154///     .stage(Box::new(CustomInputStage))
155///     .shaper(my_shaper)
156///     .renderer(my_renderer)
157///     .build()?;
158/// ```
159pub struct PipelineBuilder {
160    stages: Vec<Box<dyn Stage>>,
161    shaper: Option<Arc<dyn Shaper>>,
162    renderer: Option<Arc<dyn Renderer>>,
163    exporter: Option<Arc<dyn Exporter>>,
164    cache_policy: CachePolicy,
165    shaping_cache: Option<SharedShapingCache>,
166    glyph_cache: Option<SharedGlyphCache>,
167}
168
169impl PipelineBuilder {
170    /// Start with a clean slate
171    pub fn new() -> Self {
172        Self {
173            stages: Vec::new(),
174            shaper: None,
175            renderer: None,
176            exporter: None,
177            cache_policy: CachePolicy::default(),
178            shaping_cache: None,
179            glyph_cache: None,
180        }
181    }
182
183    /// Add your own stage to the flow
184    pub fn stage(mut self, stage: Box<dyn Stage>) -> Self {
185        self.stages.push(stage);
186        self
187    }
188
189    /// Choose who turns characters into glyphs
190    pub fn shaper(mut self, shaper: Arc<dyn Shaper>) -> Self {
191        self.shaper = Some(shaper);
192        self
193    }
194
195    /// Choose who turns glyphs into pixels
196    pub fn renderer(mut self, renderer: Arc<dyn Renderer>) -> Self {
197        self.renderer = Some(renderer);
198        self
199    }
200
201    /// Choose who packages the final output
202    pub fn exporter(mut self, exporter: Arc<dyn Exporter>) -> Self {
203        self.exporter = Some(exporter);
204        self
205    }
206
207    /// Enable or disable shaping cache (default: disabled)
208    pub fn enable_shaping_cache(mut self, enabled: bool) -> Self {
209        self.cache_policy.shaping = enabled;
210        self
211    }
212
213    /// Enable or disable glyph/render cache (default: disabled)
214    pub fn enable_glyph_cache(mut self, enabled: bool) -> Self {
215        self.cache_policy.glyph = enabled;
216        self
217    }
218
219    /// Supply a shared shaping cache for reuse across pipelines
220    pub fn with_shaping_cache(mut self, cache: SharedShapingCache) -> Self {
221        self.shaping_cache = Some(cache);
222        self
223    }
224
225    /// Supply a shared glyph cache for reuse across pipelines
226    pub fn with_glyph_cache(mut self, cache: SharedGlyphCache) -> Self {
227        self.glyph_cache = Some(cache);
228        self
229    }
230
231    /// Create the pipeline, ready to run
232    pub fn build(self) -> Result<Pipeline> {
233        // No custom stages? Use the classic six
234        let stages = if self.stages.is_empty() {
235            vec![
236                Box::new(InputParsingStage) as Box<dyn Stage>,
237                Box::new(UnicodeProcessingStage) as Box<dyn Stage>,
238                Box::new(FontSelectionStage) as Box<dyn Stage>,
239                Box::new(ShapingStage) as Box<dyn Stage>,
240                Box::new(RenderingStage) as Box<dyn Stage>,
241                Box::new(ExportStage) as Box<dyn Stage>,
242            ]
243        } else {
244            self.stages
245        };
246
247        let shaping_cache = if self.cache_policy.shaping {
248            Some(
249                self.shaping_cache
250                    .unwrap_or_else(|| Arc::new(RwLock::new(ShapingCache::new()))),
251            )
252        } else {
253            None
254        };
255
256        let glyph_cache = if self.cache_policy.glyph {
257            Some(
258                self.glyph_cache
259                    .unwrap_or_else(|| Arc::new(RwLock::new(GlyphCache::new()))),
260            )
261        } else {
262            None
263        };
264
265        let shaper = match (self.shaper, shaping_cache.as_ref()) {
266            (Some(shaper), Some(cache)) => {
267                Some(Arc::new(CachedShaper::new(shaper, cache.clone())) as Arc<dyn Shaper>)
268            },
269            (Some(shaper), None) => Some(shaper),
270            (None, _) => None,
271        };
272
273        let renderer = match (self.renderer, glyph_cache.as_ref()) {
274            (Some(renderer), Some(cache)) => {
275                Some(Arc::new(CachedRenderer::new(renderer, cache.clone())) as Arc<dyn Renderer>)
276            },
277            (Some(renderer), None) => Some(renderer),
278            (None, _) => None,
279        };
280
281        Ok(Pipeline {
282            stages,
283            shaper,
284            renderer,
285            exporter: self.exporter,
286            cache_policy: self.cache_policy,
287            shaping_cache,
288            glyph_cache,
289        })
290    }
291}
292
293impl Default for PipelineBuilder {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299// The six stages that make up the default pipeline
300// Note: The first three stages are pass-throughs reserved for future use.
301// All actual processing happens in Shaping, Rendering, and Export stages.
302
303/// Pass-through stage reserved for future input validation.
304struct InputParsingStage;
305impl Stage for InputParsingStage {
306    fn name(&self) -> &'static str {
307        "InputParsing"
308    }
309
310    fn process(&self, context: PipelineContext) -> Result<PipelineContext> {
311        log::trace!("InputParsing: pass-through (reserved for future use)");
312        Ok(context)
313    }
314}
315
316/// Pass-through stage reserved for future Unicode processing.
317/// For bidi/script handling, use `typf_unicode::UnicodeProcessor` directly.
318struct UnicodeProcessingStage;
319impl Stage for UnicodeProcessingStage {
320    fn name(&self) -> &'static str {
321        "UnicodeProcessing"
322    }
323
324    fn process(&self, context: PipelineContext) -> Result<PipelineContext> {
325        log::trace!("UnicodeProcessing: pass-through (reserved for future use)");
326        Ok(context)
327    }
328}
329
330/// Pass-through stage reserved for future font selection/fallback.
331/// For font loading, use `typf_fontdb` directly.
332struct FontSelectionStage;
333impl Stage for FontSelectionStage {
334    fn name(&self) -> &'static str {
335        "FontSelection"
336    }
337
338    fn process(&self, context: PipelineContext) -> Result<PipelineContext> {
339        log::trace!("FontSelection: pass-through (reserved for future use)");
340        Ok(context)
341    }
342}
343
344struct ShapingStage;
345impl Stage for ShapingStage {
346    fn name(&self) -> &'static str {
347        "Shaping"
348    }
349
350    fn process(&self, mut context: PipelineContext) -> Result<PipelineContext> {
351        let shaper = context
352            .shaper()
353            .ok_or_else(|| TypfError::Pipeline("No shaper configured".into()))?;
354
355        let font = context
356            .font()
357            .ok_or_else(|| TypfError::Pipeline("No font selected".into()))?;
358
359        let text = context.text();
360        let params = context.shaping_params();
361
362        log::debug!("Shaping text with backend: {}", shaper.name());
363        let shaped = shaper.shape(text, font, params)?;
364
365        context.set_shaped(shaped);
366        Ok(context)
367    }
368}
369
370struct RenderingStage;
371impl Stage for RenderingStage {
372    fn name(&self) -> &'static str {
373        "Rendering"
374    }
375
376    fn process(&self, mut context: PipelineContext) -> Result<PipelineContext> {
377        let renderer = context
378            .renderer()
379            .ok_or_else(|| TypfError::Pipeline("No renderer configured".into()))?;
380
381        let shaped = context
382            .shaped()
383            .ok_or_else(|| TypfError::Pipeline("No shaped result available".into()))?;
384
385        let font = context
386            .font()
387            .ok_or_else(|| TypfError::Pipeline("No font available".into()))?;
388
389        let params = context.render_params();
390
391        log::debug!("Rendering with backend: {}", renderer.name());
392        let output = renderer.render(shaped, font, params)?;
393
394        context.set_output(output);
395        Ok(context)
396    }
397}
398
399struct ExportStage;
400impl Stage for ExportStage {
401    fn name(&self) -> &'static str {
402        "Export"
403    }
404
405    fn process(&self, mut context: PipelineContext) -> Result<PipelineContext> {
406        if let Some(exporter) = context.exporter() {
407            let output = context
408                .output()
409                .ok_or_else(|| TypfError::Pipeline("No render output available".into()))?;
410
411            log::debug!("Exporting with backend: {}", exporter.name());
412            let exported = exporter.export(output)?;
413
414            context.set_exported(exported);
415        }
416
417        Ok(context)
418    }
419}
420
421/// Wrapper adding backend-neutral shaping cache behaviour
422struct CachedShaper {
423    inner: Arc<dyn Shaper>,
424    cache: SharedShapingCache,
425}
426
427impl CachedShaper {
428    fn new(inner: Arc<dyn Shaper>, cache: SharedShapingCache) -> Self {
429        Self { inner, cache }
430    }
431}
432
433impl Shaper for CachedShaper {
434    fn name(&self) -> &'static str {
435        self.inner.name()
436    }
437
438    fn shape(
439        &self,
440        text: &str,
441        font: Arc<dyn FontRef>,
442        params: &ShapingParams,
443    ) -> Result<crate::types::ShapingResult> {
444        let key = ShapingCacheKey::new(
445            text,
446            self.inner.name(),
447            font.data(),
448            params.size,
449            params.language.clone(),
450            params.script.clone(),
451            params.features.clone(),
452            params.variations.clone(),
453        );
454
455        if let Ok(cache) = self.cache.read() {
456            if let Some(hit) = cache.get(&key) {
457                return Ok(hit);
458            }
459        }
460
461        let shaped = self.inner.shape(text, font, params)?;
462
463        if let Ok(cache) = self.cache.write() {
464            cache.insert(key, shaped.clone());
465        }
466
467        Ok(shaped)
468    }
469}
470
471/// Wrapper adding backend-neutral render/glyph cache behaviour
472struct CachedRenderer {
473    inner: Arc<dyn Renderer>,
474    cache: SharedGlyphCache,
475}
476
477impl CachedRenderer {
478    fn new(inner: Arc<dyn Renderer>, cache: SharedGlyphCache) -> Self {
479        Self { inner, cache }
480    }
481}
482
483impl Renderer for CachedRenderer {
484    fn name(&self) -> &'static str {
485        self.inner.name()
486    }
487
488    fn render(
489        &self,
490        shaped: &crate::types::ShapingResult,
491        font: Arc<dyn FontRef>,
492        params: &RenderParams,
493    ) -> Result<crate::types::RenderOutput> {
494        let key = GlyphCacheKey::new(self.inner.name(), font.data(), shaped, params);
495
496        if let Ok(cache) = self.cache.read() {
497            if let Some(hit) = cache.get(&key) {
498                return Ok(hit);
499            }
500        }
501
502        let rendered = self.inner.render(shaped, font, params)?;
503
504        if let Ok(cache) = self.cache.write() {
505            cache.insert(key, rendered.clone());
506        }
507
508        Ok(rendered)
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::types::{
516        BitmapData, BitmapFormat, Direction, PositionedGlyph, RenderOutput, ShapingResult,
517    };
518    use std::sync::Arc;
519
520    // Mock implementations for testing
521    struct MockShaper;
522    impl Shaper for MockShaper {
523        fn name(&self) -> &'static str {
524            "MockShaper"
525        }
526        fn shape(
527            &self,
528            text: &str,
529            _font: Arc<dyn FontRef>,
530            params: &ShapingParams,
531        ) -> Result<ShapingResult> {
532            Ok(ShapingResult {
533                glyphs: text
534                    .chars()
535                    .enumerate()
536                    .map(|(i, c)| PositionedGlyph {
537                        id: c as u32,
538                        x: i as f32 * 10.0,
539                        y: 0.0,
540                        advance: 10.0,
541                        cluster: i as u32,
542                    })
543                    .collect(),
544                advance_width: text.len() as f32 * 10.0,
545                advance_height: params.size,
546                direction: Direction::LeftToRight,
547            })
548        }
549    }
550
551    struct MockRenderer;
552    impl Renderer for MockRenderer {
553        fn name(&self) -> &'static str {
554            "MockRenderer"
555        }
556        fn render(
557            &self,
558            shaped: &ShapingResult,
559            _font: Arc<dyn FontRef>,
560            _params: &RenderParams,
561        ) -> Result<RenderOutput> {
562            let width = shaped.advance_width as u32 + 1;
563            let height = shaped.advance_height as u32 + 1;
564            Ok(RenderOutput::Bitmap(BitmapData {
565                width,
566                height,
567                format: BitmapFormat::Rgba8,
568                data: vec![0u8; (width * height * 4) as usize],
569            }))
570        }
571        fn supports_format(&self, _format: &str) -> bool {
572            true
573        }
574    }
575
576    struct MockExporter;
577    impl Exporter for MockExporter {
578        fn name(&self) -> &'static str {
579            "MockExporter"
580        }
581        fn export(&self, output: &RenderOutput) -> Result<Vec<u8>> {
582            match output {
583                RenderOutput::Bitmap(bitmap) => Ok(bitmap.data.clone()),
584                _ => Ok(vec![]),
585            }
586        }
587        fn extension(&self) -> &'static str {
588            "bin"
589        }
590        fn mime_type(&self) -> &'static str {
591            "application/octet-stream"
592        }
593    }
594
595    struct MockFont;
596    impl FontRef for MockFont {
597        fn data(&self) -> &[u8] {
598            &[]
599        }
600        fn units_per_em(&self) -> u16 {
601            1000
602        }
603        fn glyph_id(&self, ch: char) -> Option<u32> {
604            Some(ch as u32)
605        }
606        fn advance_width(&self, _glyph_id: u32) -> f32 {
607            500.0
608        }
609    }
610
611    #[test]
612    fn test_pipeline_builder() {
613        let pipeline = Pipeline::builder()
614            .shaper(Arc::new(MockShaper))
615            .renderer(Arc::new(MockRenderer))
616            .exporter(Arc::new(MockExporter))
617            .build();
618
619        assert!(pipeline.is_ok());
620    }
621
622    #[test]
623    fn test_pipeline_process() {
624        let pipeline_result = Pipeline::builder()
625            .shaper(Arc::new(MockShaper))
626            .renderer(Arc::new(MockRenderer))
627            .exporter(Arc::new(MockExporter))
628            .build();
629        let pipeline = match pipeline_result {
630            Ok(pipeline) => pipeline,
631            Err(e) => {
632                unreachable!("pipeline build failed: {e}");
633            },
634        };
635
636        let font = Arc::new(MockFont);
637        let shaping_params = ShapingParams::default();
638        let render_params = RenderParams::default();
639
640        let result = pipeline.process("Hello", font, &shaping_params, &render_params);
641        match result {
642            Ok(bytes) => assert!(!bytes.is_empty()),
643            Err(e) => unreachable!("pipeline process failed: {e}"),
644        }
645    }
646
647    #[test]
648    fn test_pipeline_missing_shaper() {
649        let pipeline_result = Pipeline::builder()
650            .renderer(Arc::new(MockRenderer))
651            .exporter(Arc::new(MockExporter))
652            .build();
653        let pipeline = match pipeline_result {
654            Ok(pipeline) => pipeline,
655            Err(e) => {
656                unreachable!("pipeline build failed: {e}");
657            },
658        };
659
660        let font = Arc::new(MockFont);
661        let shaping_params = ShapingParams::default();
662        let render_params = RenderParams::default();
663
664        let result = pipeline.process("Hello", font, &shaping_params, &render_params);
665        assert!(result.is_err());
666    }
667
668    #[test]
669    fn test_pipeline_missing_renderer() {
670        let pipeline_result = Pipeline::builder()
671            .shaper(Arc::new(MockShaper))
672            .exporter(Arc::new(MockExporter))
673            .build();
674        let pipeline = match pipeline_result {
675            Ok(pipeline) => pipeline,
676            Err(e) => {
677                unreachable!("pipeline build failed: {e}");
678            },
679        };
680
681        let font = Arc::new(MockFont);
682        let shaping_params = ShapingParams::default();
683        let render_params = RenderParams::default();
684
685        let result = pipeline.process("Hello", font, &shaping_params, &render_params);
686        assert!(result.is_err());
687    }
688
689    #[test]
690    fn test_pipeline_missing_exporter() {
691        let pipeline_result = Pipeline::builder()
692            .shaper(Arc::new(MockShaper))
693            .renderer(Arc::new(MockRenderer))
694            .build();
695        let pipeline = match pipeline_result {
696            Ok(pipeline) => pipeline,
697            Err(e) => {
698                unreachable!("pipeline build failed: {e}");
699            },
700        };
701
702        let font = Arc::new(MockFont);
703        let shaping_params = ShapingParams::default();
704        let render_params = RenderParams::default();
705
706        let result = pipeline.process("Hello", font, &shaping_params, &render_params);
707        assert!(result.is_err());
708    }
709
710    #[test]
711    fn test_pipeline_execute_with_context() {
712        let pipeline_result = Pipeline::builder()
713            .shaper(Arc::new(MockShaper))
714            .renderer(Arc::new(MockRenderer))
715            .exporter(Arc::new(MockExporter))
716            .build();
717        let pipeline = match pipeline_result {
718            Ok(pipeline) => pipeline,
719            Err(e) => {
720                unreachable!("pipeline build failed: {e}");
721            },
722        };
723
724        let font = Arc::new(MockFont);
725        let mut context = PipelineContext::new("Test".to_string(), "test.ttf".to_string());
726        context.set_font(font);
727
728        let result = pipeline.execute(context);
729        assert!(result.is_ok());
730    }
731
732    #[test]
733    fn test_six_stage_pipeline() {
734        let pipeline_result = Pipeline::builder()
735            .shaper(Arc::new(MockShaper))
736            .renderer(Arc::new(MockRenderer))
737            .exporter(Arc::new(MockExporter))
738            .build();
739        let pipeline = match pipeline_result {
740            Ok(pipeline) => pipeline,
741            Err(e) => {
742                unreachable!("pipeline build failed: {e}");
743            },
744        };
745
746        // Verify all 6 stages are created
747        assert_eq!(pipeline.stages.len(), 6);
748    }
749
750    #[test]
751    fn test_pipeline_stage_names() {
752        let pipeline_result = Pipeline::builder().build();
753        let pipeline = match pipeline_result {
754            Ok(pipeline) => pipeline,
755            Err(e) => {
756                unreachable!("pipeline build failed: {e}");
757            },
758        };
759
760        let expected_stages = [
761            "InputParsing",
762            "UnicodeProcessing",
763            "FontSelection",
764            "Shaping",
765            "Rendering",
766            "Export",
767        ];
768
769        for (i, expected_name) in expected_stages.iter().enumerate() {
770            assert_eq!(pipeline.stages[i].name(), *expected_name);
771        }
772    }
773
774    #[test]
775    fn test_pipeline_empty_text() {
776        let pipeline_result = Pipeline::builder()
777            .shaper(Arc::new(MockShaper))
778            .renderer(Arc::new(MockRenderer))
779            .exporter(Arc::new(MockExporter))
780            .build();
781        let pipeline = match pipeline_result {
782            Ok(pipeline) => pipeline,
783            Err(e) => {
784                unreachable!("pipeline build failed: {e}");
785            },
786        };
787
788        let font = Arc::new(MockFont);
789        let shaping_params = ShapingParams::default();
790        let render_params = RenderParams::default();
791
792        let result = pipeline.process("", font, &shaping_params, &render_params);
793        assert!(result.is_ok());
794    }
795}