Skip to main content

typf_core/
pipeline.rs

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