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}