Skip to main content

typf_core/
lib.rs

1//! # typf-core: The Engine Room
2//!
3//! Six stages from text to pixels. No magic, just engineering.
4//!
5//! Text enters as Unicode chaos, exits as perfect pixels. This crate is the
6//! engine room that makes text rendering work when half your characters are
7//! Arabic and the other half are emoji.
8//!
9//! ## The Pipeline: Brutal Simplicity
10//!
11//! Every text walks this path, no shortcuts:
12//!
13//! 1. **Input Parsing** - Raw strings → structured data. We don't guess encoding.
14//! 2. **Unicode Processing** - Bidi, scripts, segmentation. The hard stuff.
15//! 3. **Font Selection** - Right font for each character, period. Fallback that actually works.
16//! 4. **Shaping** - Characters → positioned glyphs. Where HarfBuzz lives.
17//! 5. **Rendering** - Glyphs → pixels/vectors. SIMD or die.
18//! 6. **Export** - Your format, ready to ship.
19//!
20//! ## Build Your First Pipeline
21//!
22//! ```rust,no_run
23//! use typf_core::{Pipeline, RenderParams, ShapingParams};
24//! use std::sync::Arc;
25//!
26//! # use typf_core::traits::*;
27//! # use typf_core::context::PipelineContext;
28//! # use typf_core::error::TypfError;
29//! # struct MyShaper;
30//! # impl Stage for MyShaper {
31//! #     fn name(&self) -> &'static str { "test" }
32//! #     fn process(&self, _ctx: PipelineContext) -> Result<PipelineContext, TypfError> { unimplemented!() }
33//! # }
34//! # impl Shaper for MyShaper {
35//! #     fn name(&self) -> &'static str { "test" }
36//! #     fn shape(&self, _: &str, _: Arc<dyn FontRef>, _: &ShapingParams)
37//! #         -> typf_core::Result<typf_core::types::ShapingResult> { unimplemented!() }
38//! # }
39//! # struct MyRenderer;
40//! # impl Stage for MyRenderer {
41//! #     fn name(&self) -> &'static str { "test" }
42//! #     fn process(&self, _ctx: PipelineContext) -> Result<PipelineContext, TypfError> { unimplemented!() }
43//! # }
44//! # impl Renderer for MyRenderer {
45//! #     fn name(&self) -> &'static str { "test" }
46//! #     fn render(&self, _: &typf_core::types::ShapingResult, _: Arc<dyn FontRef>, _: &RenderParams)
47//! #         -> typf_core::Result<typf_core::types::RenderOutput> { unimplemented!() }
48//! # }
49//! # struct MyExporter;
50//! # impl Stage for MyExporter {
51//! #     fn name(&self) -> &'static str { "test" }
52//! #     fn process(&self, _ctx: PipelineContext) -> Result<PipelineContext, TypfError> { unimplemented!() }
53//! # }
54//! # impl Exporter for MyExporter {
55//! #     fn name(&self) -> &'static str { "test" }
56//! #     fn export(&self, _: &typf_core::types::RenderOutput)
57//! #         -> typf_core::Result<Vec<u8>> { unimplemented!() }
58//! #     fn extension(&self) -> &'static str { "png" }
59//! #     fn mime_type(&self) -> &'static str { "image/png" }
60//! # }
61//! # fn load_font() -> Arc<dyn FontRef> { unimplemented!() }
62//!
63//! let pipeline = Pipeline::builder()
64//!     .shaper(Arc::new(MyShaper))
65//!     .renderer(Arc::new(MyRenderer))
66//!     .exporter(Arc::new(MyExporter))
67//!     .build()?;
68//!
69//! let font = load_font();
70//! let output = pipeline.process(
71//!     "Hello, World!",
72//!     font,
73//!     &ShapingParams::default(),
74//!     &RenderParams::default(),
75//! )?;
76//! # Ok::<(), typf_core::TypfError>(())
77//! ```
78//!
79//! ## The Traits That Power Everything
80//!
81//! Want to add your own backend? Implement one of these:
82//!
83//! - [`Stage`] - The foundation every pipeline component builds upon.
84//! - [`Shaper`] - Where characters become glyphs.
85//! - [`Renderer`] - Where glyphs become images.
86//! - [`Exporter`] - Where images become files.
87//! - [`traits::FontRef`] - Your window into font data.
88//!
89//! Data flows through the types in [`types`] - these structures carry
90//! the results from one stage to the next.
91
92// this_file: libs/typf/core/src/lib.rs
93
94use std::collections::HashSet;
95
96/// Default maximum bitmap dimension (width): 16,777,216 pixels (16M)
97///
98/// This prevents memory explosions from pathological fonts or extreme render sizes.
99/// The width can be very large for long text runs.
100pub const DEFAULT_MAX_BITMAP_WIDTH: u32 = 16 * 1024 * 1024;
101
102/// Default maximum bitmap height: 16k pixels
103///
104/// Height is more strictly limited than width since vertical overflow is rarer
105/// and tall bitmaps are often pathological.
106pub const DEFAULT_MAX_BITMAP_HEIGHT: u32 = 16 * 1024;
107
108/// Default maximum total bitmap pixels: 1 Gpix
109///
110/// This caps total memory regardless of aspect ratio.
111/// A 4-megapixel RGBA8 bitmap consumes 16 MB.
112pub const DEFAULT_MAX_BITMAP_PIXELS: u64 = 1024 * 1024 * 1024;
113
114/// Get max bitmap width from environment or use default (16,777,216).
115///
116/// Set `TYPF_MAX_BITMAP_WIDTH` to override if you have infinite RAM.
117pub fn get_max_bitmap_width() -> u32 {
118    std::env::var("TYPF_MAX_BITMAP_WIDTH")
119        .ok()
120        .and_then(|s| s.parse().ok())
121        .unwrap_or(DEFAULT_MAX_BITMAP_WIDTH)
122}
123
124/// Get max bitmap height from environment or use default (16384).
125///
126/// Set `TYPF_MAX_BITMAP_HEIGHT` to override.
127pub fn get_max_bitmap_height() -> u32 {
128    std::env::var("TYPF_MAX_BITMAP_HEIGHT")
129        .ok()
130        .and_then(|s| s.parse().ok())
131        .unwrap_or(DEFAULT_MAX_BITMAP_HEIGHT)
132}
133
134/// Get max bitmap pixels from environment or use default (1 Gpix).
135///
136/// Set `TYPF_MAX_BITMAP_PIXELS` to override.
137pub fn get_max_bitmap_pixels() -> u64 {
138    std::env::var("TYPF_MAX_BITMAP_PIXELS")
139        .ok()
140        .and_then(|s| s.parse().ok())
141        .unwrap_or(DEFAULT_MAX_BITMAP_PIXELS)
142}
143
144pub mod cache;
145pub mod cache_config;
146pub mod context;
147pub mod error;
148pub mod ffi;
149pub mod glyph_cache;
150pub mod linra;
151pub mod pipeline;
152pub mod shaping_cache;
153pub mod traits;
154
155pub use context::PipelineContext;
156pub use error::{Result, TypfError};
157pub use pipeline::{Pipeline, PipelineBuilder};
158pub use traits::{Exporter, Renderer, Shaper, Stage};
159
160// =============================================================================
161// Security Limits
162// =============================================================================
163
164/// Maximum font size in pixels to prevent DoS attacks.
165///
166/// Set very high (100K px) to catch only obvious attacks while
167/// allowing legitimate large-format rendering use cases.
168pub const MAX_FONT_SIZE: f32 = 100_000.0;
169
170/// Maximum number of glyphs to render in a single operation.
171///
172/// Set very high (10M glyphs) to catch only obvious attacks while
173/// allowing legitimate bulk text processing.
174pub const MAX_GLYPH_COUNT: usize = 10_000_000;
175
176/// The data structures that power the pipeline.
177///
178/// These structs are the blood cells of the system, carrying data
179/// from the heart (shaper) to the limbs (renderer/exporter).
180pub mod types {
181    /// Unique identifier for a glyph within a font.
182    pub type GlyphId = u32;
183
184    /// Minimal, stable font-wide metrics in font units.
185    ///
186    /// These are intended for layout/baseline decisions by consumers that only have a `FontRef`.
187    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
188    pub struct FontMetrics {
189        /// Units per em (typically 1000 or 2048).
190        pub units_per_em: u16,
191        /// Distance from baseline to top of highest glyph.
192        pub ascent: i16,
193        /// Distance from baseline to bottom of lowest glyph (usually negative).
194        pub descent: i16,
195        /// Recommended gap between lines.
196        pub line_gap: i16,
197    }
198
199    /// A variable font axis definition.
200    ///
201    /// Describes one axis of variation in a variable font (e.g., weight, width, slant).
202    /// Values are in font design units unless otherwise specified.
203    #[derive(Debug, Clone, PartialEq)]
204    pub struct VariationAxis {
205        /// 4-character tag (e.g., "wght", "wdth", "slnt", "ital").
206        pub tag: String,
207        /// Human-readable name (if available from name table).
208        pub name: Option<String>,
209        /// Minimum axis value.
210        pub min_value: f32,
211        /// Default axis value.
212        pub default_value: f32,
213        /// Maximum axis value.
214        pub max_value: f32,
215        /// Whether this is a hidden axis (not shown to users).
216        pub hidden: bool,
217    }
218
219    /// Which way the text flows.
220    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221    pub enum Direction {
222        /// Standard Latin, Cyrillic, etc.
223        LeftToRight,
224        /// Arabic, Hebrew, etc.
225        RightToLeft,
226        /// Vertical CJK (not fully supported yet).
227        TopToBottom,
228        /// Vertical (rare).
229        BottomToTop,
230    }
231
232    /// A glyph that knows exactly where it belongs.
233    #[derive(Debug, Clone, PartialEq)]
234    pub struct PositionedGlyph {
235        /// The glyph ID in the font.
236        pub id: GlyphId,
237        /// X position of the glyph origin (relative to run start).
238        pub x: f32,
239        /// Y position of the glyph origin.
240        pub y: f32,
241        /// How much to advance the cursor after this glyph.
242        pub advance: f32,
243        /// The Unicode cluster index this glyph belongs to.
244        pub cluster: u32,
245    }
246
247    /// What emerges after shaping: glyphs positioned and ready to render.
248    #[derive(Debug, Clone)]
249    pub struct ShapingResult {
250        /// The list of positioned glyphs.
251        pub glyphs: Vec<PositionedGlyph>,
252        /// Total width of the shaped run.
253        pub advance_width: f32,
254        /// Total height of the shaped run (usually 0 for horizontal text).
255        pub advance_height: f32,
256        /// Overall direction of the run.
257        pub direction: Direction,
258    }
259
260    /// The forms output can take.
261    #[derive(Debug, Clone)]
262    pub enum RenderOutput {
263        /// Rasterized bitmap (PNG, PBM, etc.).
264        Bitmap(BitmapData),
265        /// Serialized vector format (SVG, PDF).
266        Vector(VectorData),
267        /// JSON representation of glyph data.
268        Json(String),
269        /// Path geometry for GPU pipelines and tessellators.
270        Geometry(GeometryData),
271    }
272
273    impl RenderOutput {
274        /// Returns the approximate heap size in bytes of this render output.
275        ///
276        /// Used by byte-weighted caches to enforce memory limits.
277        pub fn byte_size(&self) -> usize {
278            match self {
279                RenderOutput::Bitmap(b) => b.byte_size(),
280                RenderOutput::Vector(v) => v.data.len(),
281                RenderOutput::Json(s) => s.len(),
282                RenderOutput::Geometry(g) => g.byte_size(),
283            }
284        }
285
286        /// Returns true if this is geometry output suitable for GPU consumption.
287        pub fn is_geometry(&self) -> bool {
288            matches!(self, RenderOutput::Geometry(_))
289        }
290    }
291
292    /// Raw pixel data from rasterized glyphs
293    #[derive(Debug, Clone)]
294    pub struct BitmapData {
295        pub width: u32,
296        pub height: u32,
297        pub format: BitmapFormat,
298        pub data: Vec<u8>,
299    }
300
301    impl BitmapData {
302        /// Returns the heap size in bytes of the pixel data.
303        pub fn byte_size(&self) -> usize {
304            self.data.len()
305        }
306    }
307
308    /// How pixels are arranged in the bitmap
309    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
310    pub enum BitmapFormat {
311        Rgba8,
312        Rgb8,
313        Gray8,
314        Gray1,
315    }
316
317    /// Scalable paths instead of pixels
318    #[derive(Debug, Clone)]
319    pub struct VectorData {
320        pub format: VectorFormat,
321        pub data: String,
322    }
323
324    /// Which vector format we're speaking
325    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
326    pub enum VectorFormat {
327        Svg,
328        Pdf,
329    }
330
331    // =========================================================================
332    // Stage 5 Geometry Types (for GPU pipelines and external tessellators)
333    // =========================================================================
334
335    /// A single path operation for vector glyph outlines.
336    ///
337    /// These primitives match the common subset of path operations supported by
338    /// Cairo, Skia, CoreGraphics, Direct2D, and wgpu tessellators.
339    ///
340    /// Coordinates are in font units (typically 1000 or 2048 per em).
341    /// Scale by `(font_size / units_per_em)` to convert to pixels.
342    #[derive(Debug, Clone, Copy, PartialEq)]
343    pub enum PathOp {
344        /// Move to a new position without drawing (M x y)
345        MoveTo { x: f32, y: f32 },
346        /// Draw a straight line to a point (L x y)
347        LineTo { x: f32, y: f32 },
348        /// Quadratic Bézier curve (Q cx cy x y)
349        QuadTo { cx: f32, cy: f32, x: f32, y: f32 },
350        /// Cubic Bézier curve (C c1x c1y c2x c2y x y)
351        CubicTo {
352            c1x: f32,
353            c1y: f32,
354            c2x: f32,
355            c2y: f32,
356            x: f32,
357            y: f32,
358        },
359        /// Close the current subpath (Z)
360        Close,
361    }
362
363    /// Glyph path data with positioning for a single glyph.
364    ///
365    /// Contains the vector outline of a glyph and its rendered position.
366    /// Can be consumed by external tessellators or GPU pipelines.
367    #[derive(Debug, Clone, PartialEq)]
368    pub struct GlyphPath {
369        /// Glyph ID in the font
370        pub glyph_id: GlyphId,
371        /// X position of the glyph origin (in rendered coordinates)
372        pub x: f32,
373        /// Y position of the glyph origin (in rendered coordinates)
374        pub y: f32,
375        /// Path operations defining the glyph outline (in font units)
376        pub ops: Vec<PathOp>,
377    }
378
379    /// Geometry output for GPU pipelines and vector consumers.
380    ///
381    /// This provides path operations that can be:
382    /// - Tessellated by external libraries (lyon, earcutr)
383    /// - Converted to GPU meshes for wgpu/Vulkan
384    /// - Used for hit testing and text selection
385    /// - Exported to vector formats
386    ///
387    /// # Coordinate Systems
388    ///
389    /// - Path ops are in font units (unscaled)
390    /// - Glyph positions (x, y) are in rendered coordinates (scaled to font_size)
391    /// - Consumer must apply `font_size / units_per_em` scaling to path ops
392    #[derive(Debug, Clone)]
393    pub struct GeometryData {
394        /// Glyph paths with their positions
395        pub glyphs: Vec<GlyphPath>,
396        /// Total advance width in rendered coordinates
397        pub advance_width: f32,
398        /// Total advance height in rendered coordinates
399        pub advance_height: f32,
400        /// Font units per em (for scaling path ops to rendered coordinates)
401        pub units_per_em: u16,
402        /// Font size used for positioning (pixels)
403        pub font_size: f32,
404    }
405
406    impl GeometryData {
407        /// Returns the approximate heap size in bytes.
408        pub fn byte_size(&self) -> usize {
409            self.glyphs
410                .iter()
411                .map(|g| {
412                    std::mem::size_of::<GlyphPath>() + g.ops.len() * std::mem::size_of::<PathOp>()
413                })
414                .sum()
415        }
416
417        /// Returns an iterator over glyph paths.
418        pub fn iter(&self) -> impl Iterator<Item = &GlyphPath> {
419            self.glyphs.iter()
420        }
421
422        /// Scale factor to convert font units to rendered coordinates.
423        pub fn scale(&self) -> f32 {
424            self.font_size / self.units_per_em as f32
425        }
426    }
427
428    /// The source type of glyph data in a font
429    ///
430    /// Different glyph types require different rendering approaches:
431    /// - Outlines can be scaled and exported to SVG paths
432    /// - Bitmaps are pre-rendered at specific sizes
433    /// - COLR/SVG glyphs contain color information
434    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
435    pub enum GlyphType {
436        /// Standard vector outline (glyf/CFF/CFF2 tables)
437        Outline,
438        /// Color glyph using COLR/CPAL tables (v0 or v1)
439        Colr,
440        /// Color glyph using embedded SVG documents
441        Svg,
442        /// Embedded bitmap glyph (CBDT/CBLC tables - Google format)
443        BitmapCbdt,
444        /// Embedded bitmap glyph (EBDT/EBLC tables - legacy format)
445        BitmapEbdt,
446        /// Embedded bitmap glyph (sbix table - Apple format)
447        BitmapSbix,
448        /// Glyph with no data (space, missing glyph, etc.)
449        Empty,
450    }
451
452    impl GlyphType {
453        /// Returns true if this glyph type contains vector outline data
454        pub fn has_outline(&self) -> bool {
455            matches!(self, GlyphType::Outline | GlyphType::Colr)
456        }
457
458        /// Returns true if this glyph type contains bitmap data
459        pub fn is_bitmap(&self) -> bool {
460            matches!(
461                self,
462                GlyphType::BitmapCbdt | GlyphType::BitmapEbdt | GlyphType::BitmapSbix
463            )
464        }
465
466        /// Returns true if this glyph type contains color information
467        pub fn is_color(&self) -> bool {
468            matches!(
469                self,
470                GlyphType::Colr | GlyphType::Svg | GlyphType::BitmapCbdt | GlyphType::BitmapSbix
471            )
472        }
473    }
474
475    /// How text gets broken into manageable pieces
476    #[derive(Debug, Clone)]
477    pub struct SegmentOptions {
478        pub language: Option<String>,
479        pub bidi_resolve: bool,
480        pub font_fallback: bool,
481        pub script_itemize: bool,
482    }
483
484    impl Default for SegmentOptions {
485        fn default() -> Self {
486            Self {
487                language: None,
488                bidi_resolve: true,
489                font_fallback: false,
490                script_itemize: true,
491            }
492        }
493    }
494
495    /// Text that shares the same script, direction, and language
496    #[derive(Debug, Clone)]
497    pub struct TextRun {
498        pub text: String,
499        pub start: usize,
500        pub end: usize,
501        pub script: icu_properties::props::Script,
502        pub language: String,
503        pub direction: Direction,
504    }
505}
506
507/// How shaping should behave
508#[derive(Debug, Clone)]
509pub struct ShapingParams {
510    pub size: f32,
511    pub direction: types::Direction,
512    pub language: Option<String>,
513    pub script: Option<String>,
514    pub features: Vec<(String, u32)>,
515    pub variations: Vec<(String, f32)>,
516    pub letter_spacing: f32,
517}
518
519impl Default for ShapingParams {
520    fn default() -> Self {
521        Self {
522            size: 16.0,
523            direction: types::Direction::LeftToRight,
524            language: None,
525            script: None,
526            features: Vec::new(),
527            variations: Vec::new(),
528            letter_spacing: 0.0,
529        }
530    }
531}
532
533impl ShapingParams {
534    /// Validate shaping parameters against security limits
535    ///
536    /// Returns an error if:
537    /// - Font size is not finite (`NaN`, `+/-inf`)
538    /// - Font size exceeds [`MAX_FONT_SIZE`] (currently 100,000 pixels)
539    /// - Font size is negative or zero
540    ///
541    /// # Example
542    ///
543    /// ```
544    /// use typf_core::ShapingParams;
545    ///
546    /// let params = ShapingParams { size: 48.0, ..Default::default() };
547    /// params.validate().expect("valid params");
548    ///
549    /// let bad_params = ShapingParams { size: 200_000.0, ..Default::default() };
550    /// assert!(bad_params.validate().is_err());
551    /// ```
552    pub fn validate(&self) -> Result<(), error::ShapingError> {
553        if !self.size.is_finite() {
554            return Err(error::ShapingError::BackendError(
555                "Font size must be finite".to_string(),
556            ));
557        }
558        if self.size <= 0.0 {
559            return Err(error::ShapingError::BackendError(
560                "Font size must be positive".to_string(),
561            ));
562        }
563        if self.size > MAX_FONT_SIZE {
564            return Err(error::ShapingError::FontSizeTooLarge(
565                self.size,
566                MAX_FONT_SIZE,
567            ));
568        }
569        Ok(())
570    }
571}
572
573/// Validate glyph count against security limits
574///
575/// Returns an error if glyph count exceeds [`MAX_GLYPH_COUNT`] (currently 10M).
576/// Call this before rendering to prevent resource exhaustion from malicious input.
577///
578/// # Example
579///
580/// ```
581/// use typf_core::{validate_glyph_count, types::ShapingResult};
582///
583/// // In a renderer, before processing:
584/// // validate_glyph_count(shaped.glyphs.len())?;
585/// ```
586pub fn validate_glyph_count(count: usize) -> Result<(), error::RenderError> {
587    if count > MAX_GLYPH_COUNT {
588        return Err(error::RenderError::GlyphCountTooLarge(
589            count,
590            MAX_GLYPH_COUNT,
591        ));
592    }
593    Ok(())
594}
595
596/// Which glyph data sources are allowed and in what order
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
598pub enum GlyphSource {
599    Glyf,
600    Cff,
601    Cff2,
602    Colr0,
603    Colr1,
604    Svg,
605    Sbix,
606    Cbdt,
607    Ebdt,
608}
609
610const DEFAULT_GLYPH_SOURCES: [GlyphSource; 9] = [
611    GlyphSource::Glyf,
612    GlyphSource::Cff2,
613    GlyphSource::Cff,
614    GlyphSource::Colr1,
615    GlyphSource::Colr0,
616    GlyphSource::Svg,
617    GlyphSource::Sbix,
618    GlyphSource::Cbdt,
619    GlyphSource::Ebdt,
620];
621
622/// Preference ordering and deny list for glyph sources
623#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct GlyphSourcePreference {
625    pub prefer: Vec<GlyphSource>,
626    pub deny: HashSet<GlyphSource>,
627}
628
629impl GlyphSourcePreference {
630    /// Build a preference list with an optional deny set.
631    ///
632    /// - Empty `prefer` uses the default outline-first order.
633    /// - Duplicates are removed while keeping first-seen order.
634    /// - Denied sources are removed from the preferred list.
635    pub fn from_parts(
636        prefer: Vec<GlyphSource>,
637        deny: impl IntoIterator<Item = GlyphSource>,
638    ) -> Self {
639        let deny: HashSet<GlyphSource> = deny.into_iter().collect();
640        let source_order = if prefer.is_empty() {
641            DEFAULT_GLYPH_SOURCES.to_vec()
642        } else {
643            prefer
644        };
645
646        let mut seen = HashSet::new();
647        let mut normalized = Vec::new();
648
649        for source in source_order {
650            if deny.contains(&source) {
651                continue;
652            }
653            if seen.insert(source) {
654                normalized.push(source);
655            }
656        }
657
658        Self {
659            prefer: normalized,
660            deny,
661        }
662    }
663
664    /// Effective order with current denies applied.
665    pub fn effective_order(&self) -> Vec<GlyphSource> {
666        self.prefer
667            .iter()
668            .copied()
669            .filter(|src| !self.deny.contains(src))
670            .collect()
671    }
672}
673
674impl Default for GlyphSourcePreference {
675    fn default() -> Self {
676        Self::from_parts(DEFAULT_GLYPH_SOURCES.to_vec(), [])
677    }
678}
679
680/// How rendering should look
681#[derive(Debug, Clone)]
682pub struct RenderParams {
683    pub foreground: Color,
684    pub background: Option<Color>,
685    pub padding: u32,
686    pub antialias: bool,
687    /// Variable font variations like [("wght", 700.0), ("wdth", 100.0)]
688    /// Variable fonts need specific coordinates to render correctly
689    pub variations: Vec<(String, f32)>,
690    /// CPAL color palette index for COLR color glyphs (0 = default palette)
691    pub color_palette: u16,
692    /// Allowed glyph sources (order + deny list)
693    pub glyph_sources: GlyphSourcePreference,
694    /// Desired render output mode (bitmap or vector)
695    pub output: RenderMode,
696}
697
698impl Default for RenderParams {
699    fn default() -> Self {
700        Self {
701            foreground: Color::black(),
702            background: None,
703            padding: 0,
704            antialias: true,
705            variations: Vec::new(),
706            color_palette: 0,
707            glyph_sources: GlyphSourcePreference::default(),
708            output: RenderMode::Bitmap,
709        }
710    }
711}
712
713/// Target output for rendering operations
714#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
715pub enum RenderMode {
716    /// Raster output (default)
717    Bitmap,
718    /// Vector output (currently SVG only)
719    Vector(types::VectorFormat),
720}
721
722/// Simple RGBA color that works everywhere
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
724pub struct Color {
725    pub r: u8,
726    pub g: u8,
727    pub b: u8,
728    pub a: u8,
729}
730
731impl Color {
732    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
733        Self { r, g, b, a }
734    }
735
736    pub const fn black() -> Self {
737        Self::rgba(0, 0, 0, 255)
738    }
739
740    pub const fn white() -> Self {
741        Self::rgba(255, 255, 255, 255)
742    }
743}
744
745#[cfg(test)]
746#[allow(clippy::expect_used, clippy::panic)]
747mod tests {
748    use super::types::*;
749    use super::{error::ShapingError, ShapingParams, MAX_FONT_SIZE};
750
751    #[test]
752    fn test_path_op_size() {
753        // PathOp should be reasonably compact for efficient storage
754        assert!(std::mem::size_of::<PathOp>() <= 32);
755    }
756
757    #[test]
758    fn test_geometry_data_byte_size() {
759        let geometry = GeometryData {
760            glyphs: vec![
761                GlyphPath {
762                    glyph_id: 1,
763                    x: 0.0,
764                    y: 0.0,
765                    ops: vec![
766                        PathOp::MoveTo { x: 0.0, y: 0.0 },
767                        PathOp::LineTo { x: 100.0, y: 0.0 },
768                        PathOp::Close,
769                    ],
770                },
771                GlyphPath {
772                    glyph_id: 2,
773                    x: 50.0,
774                    y: 0.0,
775                    ops: vec![
776                        PathOp::MoveTo { x: 0.0, y: 0.0 },
777                        PathOp::QuadTo {
778                            cx: 50.0,
779                            cy: 100.0,
780                            x: 100.0,
781                            y: 0.0,
782                        },
783                        PathOp::Close,
784                    ],
785                },
786            ],
787            advance_width: 100.0,
788            advance_height: 0.0,
789            units_per_em: 1000,
790            font_size: 16.0,
791        };
792
793        // byte_size should be non-zero and reasonable
794        let size = geometry.byte_size();
795        assert!(size > 0);
796        assert!(size < 1024); // Should be small for this test case
797    }
798
799    #[test]
800    fn test_geometry_data_scale() {
801        let geometry = GeometryData {
802            glyphs: vec![],
803            advance_width: 0.0,
804            advance_height: 0.0,
805            units_per_em: 2048,
806            font_size: 16.0,
807        };
808
809        let scale = geometry.scale();
810        assert!((scale - 16.0 / 2048.0).abs() < 0.0001);
811    }
812
813    #[test]
814    fn test_render_output_geometry_variant() {
815        let geometry = GeometryData {
816            glyphs: vec![GlyphPath {
817                glyph_id: 42,
818                x: 0.0,
819                y: 0.0,
820                ops: vec![PathOp::MoveTo { x: 0.0, y: 0.0 }, PathOp::Close],
821            }],
822            advance_width: 50.0,
823            advance_height: 0.0,
824            units_per_em: 1000,
825            font_size: 24.0,
826        };
827
828        let output = RenderOutput::Geometry(geometry);
829        assert!(output.is_geometry());
830        assert!(output.byte_size() > 0);
831    }
832
833    #[test]
834    fn test_shaping_params_validate_when_non_finite_size_then_error() {
835        for size in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
836            let params = ShapingParams {
837                size,
838                ..Default::default()
839            };
840            let error = params.validate().expect_err("non-finite size must fail");
841            match error {
842                ShapingError::BackendError(message) => {
843                    assert!(
844                        message.contains("finite"),
845                        "expected finite-size guidance, got: {}",
846                        message
847                    );
848                },
849                other => panic!("expected BackendError, got {:?}", other),
850            }
851        }
852    }
853
854    #[test]
855    fn test_shaping_params_validate_when_positive_size_then_ok() {
856        let params = ShapingParams {
857            size: 24.0,
858            ..Default::default()
859        };
860        params
861            .validate()
862            .expect("positive finite size should validate");
863    }
864
865    #[test]
866    fn test_shaping_params_validate_when_size_above_max_then_error() {
867        let params = ShapingParams {
868            size: MAX_FONT_SIZE + 1.0,
869            ..Default::default()
870        };
871        let error = params
872            .validate()
873            .expect_err("oversized font size must fail validation");
874        match error {
875            ShapingError::FontSizeTooLarge(size, max) => {
876                assert!(
877                    size > max,
878                    "expected reported size {} to exceed max {}",
879                    size,
880                    max
881                );
882            },
883            other => panic!("expected FontSizeTooLarge, got {:?}", other),
884        }
885    }
886}