1use 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
15pub 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
73pub struct CachePolicy {
74 pub shaping: bool,
75 pub glyph: bool,
76}
77
78impl Pipeline {
79 pub fn builder() -> PipelineBuilder {
81 PipelineBuilder::new()
82 }
83
84 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 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 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 pub fn execute(&self, mut context: PipelineContext) -> Result<PipelineContext> {
116 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 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
137pub 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 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 pub fn stage(mut self, stage: Box<dyn Stage>) -> Self {
185 self.stages.push(stage);
186 self
187 }
188
189 pub fn shaper(mut self, shaper: Arc<dyn Shaper>) -> Self {
191 self.shaper = Some(shaper);
192 self
193 }
194
195 pub fn renderer(mut self, renderer: Arc<dyn Renderer>) -> Self {
197 self.renderer = Some(renderer);
198 self
199 }
200
201 pub fn exporter(mut self, exporter: Arc<dyn Exporter>) -> Self {
203 self.exporter = Some(exporter);
204 self
205 }
206
207 pub fn enable_shaping_cache(mut self, enabled: bool) -> Self {
209 self.cache_policy.shaping = enabled;
210 self
211 }
212
213 pub fn enable_glyph_cache(mut self, enabled: bool) -> Self {
215 self.cache_policy.glyph = enabled;
216 self
217 }
218
219 pub fn with_shaping_cache(mut self, cache: SharedShapingCache) -> Self {
221 self.shaping_cache = Some(cache);
222 self
223 }
224
225 pub fn with_glyph_cache(mut self, cache: SharedGlyphCache) -> Self {
227 self.glyph_cache = Some(cache);
228 self
229 }
230
231 pub fn build(self) -> Result<Pipeline> {
233 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
299struct 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
316struct 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
330struct 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
421struct 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
471struct 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 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 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}