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