1use 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
19pub 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 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
114pub 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 pub fn stage(mut self, stage: Box<dyn Stage>) -> Self {
161 self.stages.push(stage);
162 self
163 }
164
165 pub fn shaper(mut self, shaper: Arc<dyn Shaper>) -> Self {
167 self.shaper = Some(shaper);
168 self
169 }
170
171 pub fn renderer(mut self, renderer: Arc<dyn Renderer>) -> Self {
173 self.renderer = Some(renderer);
174 self
175 }
176
177 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 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 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 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}