1#![cfg_attr(not(feature = "std"), no_std)]
8#![forbid(unsafe_code)]
9#![warn(missing_docs)]
10
11#[cfg(not(feature = "std"))]
12extern crate alloc;
13
14#[cfg(not(feature = "std"))]
15use alloc::{string::String, sync::Arc, vec, vec::Vec};
16#[cfg(feature = "std")]
17use std::sync::Arc;
18
19use smallvec::SmallVec;
20
21#[cfg(feature = "png-encode")]
26pub mod png_encode;
27
28#[derive(Debug, Clone)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct ShapedGlyph {
32 pub gid: u16,
34 pub x_advance: f32,
36 pub y_advance: f32,
38 pub x_offset: f32,
40 pub y_offset: f32,
42 pub cluster: u32,
44 pub is_whitespace: bool,
49 pub unsafe_to_break: bool,
53}
54
55impl Default for ShapedGlyph {
56 fn default() -> Self {
58 Self {
59 gid: 0,
60 x_advance: 0.0,
61 y_advance: 0.0,
62 x_offset: 0.0,
63 y_offset: 0.0,
64 cluster: 0,
65 is_whitespace: false,
66 unsafe_to_break: false,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub struct FontVerticalMetrics {
84 pub units_per_em: u16,
86 pub ascender: i16,
88 pub descender: i16,
90 pub line_gap: i16,
92}
93
94impl FontVerticalMetrics {
95 pub fn ascent_px(&self, font_size_px: f32) -> f32 {
97 if self.units_per_em == 0 {
98 return font_size_px * 0.8;
99 }
100 self.ascender as f32 * font_size_px / self.units_per_em as f32
101 }
102
103 pub fn descent_px(&self, font_size_px: f32) -> f32 {
105 if self.units_per_em == 0 {
106 return font_size_px * 0.2;
107 }
108 (-(self.descender as f32)) * font_size_px / self.units_per_em as f32
109 }
110
111 pub fn line_gap_px(&self, font_size_px: f32) -> f32 {
113 if self.units_per_em == 0 {
114 return font_size_px * 0.4;
115 }
116 self.line_gap as f32 * font_size_px / self.units_per_em as f32
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129pub struct GlyphMetrics {
130 pub bearing_x: f32,
132 pub bearing_y: f32,
134 pub advance_x: f32,
136 pub advance_y: f32,
138 pub width: f32,
140 pub height: f32,
142}
143
144impl Default for GlyphMetrics {
145 fn default() -> Self {
146 Self {
147 bearing_x: 0.0,
148 bearing_y: 0.0,
149 advance_x: 0.0,
150 advance_y: 0.0,
151 width: 0.0,
152 height: 0.0,
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
164pub struct GlyphCluster {
165 pub glyphs: Vec<ShapedGlyph>,
167 pub source_start: u32,
169 pub source_end: u32,
171}
172
173impl GlyphCluster {
174 pub fn advance(&self) -> f32 {
176 self.glyphs.iter().map(|g| g.x_advance).sum()
177 }
178
179 pub fn is_empty(&self) -> bool {
181 self.glyphs.is_empty()
182 }
183}
184
185#[derive(Debug, Clone)]
187pub struct ShapedRun {
188 pub glyphs: SmallVec<[ShapedGlyph; 8]>,
193 pub font_data: Arc<[u8]>,
195}
196
197#[derive(Debug, Clone)]
199pub struct PositionedGlyph {
200 pub gid: u16,
202 pub font_data: Arc<[u8]>,
204 pub pos: (f32, f32),
206 pub font_size: f32,
212 pub advance_x: f32,
217 pub cluster: u32,
223}
224
225#[derive(Debug, Clone)]
227#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
228pub struct Bitmap {
229 pub width: u32,
231 pub height: u32,
233 pub pixels: Vec<u8>,
235}
236
237impl Bitmap {
238 pub fn is_empty(&self) -> bool {
241 self.width == 0 || self.height == 0 || self.pixels.is_empty()
242 }
243
244 pub fn invert_coverage(&self) -> Self {
250 Bitmap {
251 width: self.width,
252 height: self.height,
253 pixels: self.pixels.iter().map(|&v| 255 - v).collect(),
254 }
255 }
256
257 pub fn threshold(&self, threshold: u8) -> Self {
263 Bitmap {
264 width: self.width,
265 height: self.height,
266 pixels: self
267 .pixels
268 .iter()
269 .map(|&v| if v >= threshold { 255 } else { 0 })
270 .collect(),
271 }
272 }
273
274 pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> Self {
277 let mut pixels = vec![0u8; (width * height) as usize];
278 for row in 0..height {
279 for col in 0..width {
280 let src_x = x + col;
281 let src_y = y + row;
282 if src_x < self.width && src_y < self.height {
283 let src_idx = (src_y * self.width + src_x) as usize;
284 let dst_idx = (row * width + col) as usize;
285 pixels[dst_idx] = self.pixels[src_idx];
286 }
287 }
288 }
289 Bitmap {
290 width,
291 height,
292 pixels,
293 }
294 }
295
296 pub fn tight_bounds(&self) -> Option<(u32, u32, u32, u32)> {
302 let mut x_min = self.width;
303 let mut y_min = self.height;
304 let mut x_max = 0u32;
305 let mut y_max = 0u32;
306
307 for row in 0..self.height {
308 for col in 0..self.width {
309 if self.pixels[(row * self.width + col) as usize] > 0 {
310 x_min = x_min.min(col);
311 y_min = y_min.min(row);
312 x_max = x_max.max(col);
313 y_max = y_max.max(row);
314 }
315 }
316 }
317
318 if x_min > x_max {
319 None
320 } else {
321 Some((x_min, y_min, x_max, y_max))
322 }
323 }
324}
325
326#[derive(Debug, Clone)]
331#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
332pub struct ColorBitmap {
333 pub width: u32,
335 pub height: u32,
337 pub rgba: Vec<u8>,
339}
340
341impl ColorBitmap {
342 pub fn is_empty(&self) -> bool {
344 self.width == 0 || self.height == 0 || self.rgba.is_empty()
345 }
346}
347
348#[derive(Debug, Clone)]
357#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
358pub struct LcdBitmap {
359 pub width: u32,
361 pub height: u32,
363 pub rgb: Vec<u8>,
365}
366
367impl LcdBitmap {
368 pub fn new(width: u32, height: u32, rgb: Vec<u8>) -> Self {
375 debug_assert_eq!(
376 rgb.len(),
377 (width as usize) * (height as usize) * 3,
378 "LcdBitmap: rgb buffer length must equal width * height * 3"
379 );
380 Self { width, height, rgb }
381 }
382
383 pub fn is_empty(&self) -> bool {
385 self.width == 0 || self.height == 0 || self.rgb.is_empty()
386 }
387}
388
389#[derive(Debug, Clone)]
395#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
396pub enum RenderOutput {
397 Greyscale(Bitmap),
399 Color(ColorBitmap),
401 Sdf {
403 width: u32,
405 height: u32,
407 data: Vec<u8>,
409 },
410 Lcd(LcdBitmap),
415 Msdf {
421 width: u32,
423 height: u32,
425 data: Vec<u8>,
427 },
428}
429
430#[derive(Debug, Clone)]
432#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
433pub struct LayoutConstraints {
434 pub max_width: f32,
436 pub font_size: f32,
438}
439
440impl Default for LayoutConstraints {
441 fn default() -> Self {
442 Self {
443 max_width: 800.0,
444 font_size: 16.0,
445 }
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
455#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
456pub enum FlowDirection {
457 #[default]
459 Horizontal,
460 Vertical,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
468#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
469pub enum TextAlignment {
470 #[default]
472 Left,
473 Right,
475 Center,
477 Justify,
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
489#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
490pub enum WritingMode {
491 #[default]
493 HorizontalTb,
494 VerticalRl,
496 VerticalLr,
498}
499
500impl WritingMode {
501 pub fn flow_direction(self) -> FlowDirection {
503 match self {
504 WritingMode::HorizontalTb => FlowDirection::Horizontal,
505 WritingMode::VerticalRl | WritingMode::VerticalLr => FlowDirection::Vertical,
506 }
507 }
508
509 pub fn is_vertical(self) -> bool {
511 !matches!(self, WritingMode::HorizontalTb)
512 }
513}
514
515#[derive(Debug, Clone, Copy, PartialEq)]
521#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
522pub struct LineSpacing {
523 pub leading: f32,
525 pub line_height_multiplier: f32,
527}
528
529impl Default for LineSpacing {
530 fn default() -> Self {
531 Self {
532 leading: 0.0,
533 line_height_multiplier: 1.0,
534 }
535 }
536}
537
538impl LineSpacing {
539 pub fn resolve(&self, natural_line_height: f32) -> f32 {
541 natural_line_height * self.line_height_multiplier + self.leading
542 }
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
547#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
548pub struct Rgba8 {
549 pub r: u8,
551 pub g: u8,
553 pub b: u8,
555 pub a: u8,
557}
558
559impl Rgba8 {
560 pub const BLACK: Rgba8 = Rgba8 {
562 r: 0,
563 g: 0,
564 b: 0,
565 a: 255,
566 };
567 pub const TRANSPARENT: Rgba8 = Rgba8 {
569 r: 0,
570 g: 0,
571 b: 0,
572 a: 0,
573 };
574
575 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
577 Self { r, g, b, a }
578 }
579}
580
581impl Default for Rgba8 {
582 fn default() -> Self {
583 Rgba8::BLACK
584 }
585}
586
587#[derive(Debug, Clone, Copy, PartialEq)]
591#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
592pub struct DecorationLine {
593 pub position: f32,
597 pub thickness: f32,
599 pub color: Rgba8,
601}
602
603#[derive(Debug, Clone, Copy, PartialEq)]
609#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
610pub enum TextDecoration {
611 Underline {
613 color: Rgba8,
615 thickness: f32,
617 offset: f32,
619 },
620 Overline {
622 color: Rgba8,
624 thickness: f32,
626 offset: f32,
629 },
630 Strikethrough {
633 color: Rgba8,
635 thickness: f32,
637 },
638}
639
640#[derive(Debug, Clone, Copy, PartialEq)]
648#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
649pub struct DecorationRect {
650 pub x: f32,
652 pub y: f32,
654 pub width: f32,
656 pub height: f32,
658 pub color: Rgba8,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Default)]
667#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
668pub struct Decoration {
669 pub underline: Option<DecorationLine>,
671 pub overline: Option<DecorationLine>,
673 pub strikethrough: Option<DecorationLine>,
675}
676
677impl Decoration {
678 pub fn any(&self) -> bool {
680 self.underline.is_some() || self.overline.is_some() || self.strikethrough.is_some()
681 }
682}
683
684#[derive(Debug, Clone)]
686#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
687pub struct TextStyle {
688 pub font_size: f32,
690 pub max_width: f32,
692 pub flow_direction: FlowDirection,
694 pub alignment: TextAlignment,
696 pub line_spacing: LineSpacing,
698}
699
700impl Default for TextStyle {
701 fn default() -> Self {
702 Self {
703 font_size: 16.0,
704 max_width: 800.0,
705 flow_direction: FlowDirection::Horizontal,
706 alignment: TextAlignment::Left,
707 line_spacing: LineSpacing::default(),
708 }
709 }
710}
711
712impl TextStyle {
713 pub fn with_alignment(mut self, alignment: TextAlignment) -> Self {
715 self.alignment = alignment;
716 self
717 }
718
719 pub fn with_font_size(mut self, font_size: f32) -> Self {
721 self.font_size = font_size;
722 self
723 }
724
725 pub fn with_max_width(mut self, max_width: f32) -> Self {
727 self.max_width = max_width;
728 self
729 }
730
731 pub fn with_flow_direction(mut self, flow_direction: FlowDirection) -> Self {
733 self.flow_direction = flow_direction;
734 self
735 }
736}
737
738#[derive(Debug, Clone)]
743#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
744pub struct ParagraphStyle {
745 pub alignment: TextAlignment,
747 pub indent: f32,
749 pub spacing_before: f32,
751 pub spacing_after: f32,
753 pub direction: FlowDirection,
755 pub line_spacing: LineSpacing,
757}
758
759impl Default for ParagraphStyle {
760 fn default() -> Self {
761 Self {
762 alignment: TextAlignment::Left,
763 indent: 0.0,
764 spacing_before: 0.0,
765 spacing_after: 0.0,
766 direction: FlowDirection::Horizontal,
767 line_spacing: LineSpacing::default(),
768 }
769 }
770}
771
772#[derive(Debug, Clone)]
778pub struct TextRun {
779 pub text: String,
781 pub font_data: Arc<[u8]>,
783 pub style: TextStyle,
785 pub decoration: Decoration,
787}
788
789#[derive(Debug, Clone, PartialEq)]
792pub struct InlineObject {
793 pub id: u64,
795 pub width: f32,
797 pub height: f32,
799 pub baseline_offset: f32,
801 pub advance: f32,
803}
804
805#[derive(Debug, Clone, PartialEq)]
807pub struct PositionedInlineObject {
808 pub object: InlineObject,
810 pub x: f32,
812 pub y: f32,
814 pub line: usize,
816}
817
818#[derive(Debug, Clone, Copy, PartialEq, Default)]
820pub enum VerticalPosition {
821 #[default]
823 Normal,
824 Superscript {
826 size_ratio: f32,
828 baseline_rise: f32,
830 },
831 Subscript {
833 size_ratio: f32,
835 baseline_drop: f32,
837 },
838}
839
840impl VerticalPosition {
841 pub fn effective_size(&self, base_px: f32) -> f32 {
843 match self {
844 Self::Normal => base_px,
845 Self::Superscript { size_ratio, .. } => base_px * size_ratio,
846 Self::Subscript { size_ratio, .. } => base_px * size_ratio,
847 }
848 }
849
850 pub fn baseline_adjustment(&self, _base_px: f32) -> f32 {
852 match self {
853 Self::Normal => 0.0,
854 Self::Superscript { baseline_rise, .. } => *baseline_rise,
855 Self::Subscript { baseline_drop, .. } => -*baseline_drop,
856 }
857 }
858}
859
860#[derive(Debug)]
862pub enum OxiTextError {
863 Shaping(String),
865 Layout(String),
867 Raster(String),
869 FontNotFound,
871 InvalidFont,
873 Other(String),
875}
876
877impl core::fmt::Display for OxiTextError {
878 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
879 match self {
880 OxiTextError::Shaping(s) => write!(f, "shaping error: {s}"),
881 OxiTextError::Layout(s) => write!(f, "layout error: {s}"),
882 OxiTextError::Raster(s) => write!(f, "raster error: {s}"),
883 OxiTextError::FontNotFound => write!(f, "font not found"),
884 OxiTextError::InvalidFont => write!(f, "invalid font"),
885 OxiTextError::Other(s) => write!(f, "text error: {s}"),
886 }
887 }
888}
889
890impl core::error::Error for OxiTextError {}
891
892impl RenderOutput {
893 pub fn into_bitmap(self) -> Option<Bitmap> {
896 match self {
897 RenderOutput::Greyscale(b) => Some(b),
898 _ => None,
899 }
900 }
901}
902
903impl From<RenderOutput> for Option<Bitmap> {
904 fn from(output: RenderOutput) -> Self {
907 output.into_bitmap()
908 }
909}
910
911#[cfg(all(test, feature = "std"))]
912mod tests {
913 use super::*;
914 use std::sync::Arc;
915
916 #[test]
917 fn layout_constraints_default_values() {
918 let c = LayoutConstraints::default();
919 assert_eq!(c.max_width, 800.0);
920 assert_eq!(c.font_size, 16.0);
921 }
922
923 #[test]
924 fn text_style_default_values() {
925 let s = TextStyle::default();
926 assert_eq!(s.font_size, 16.0);
927 assert_eq!(s.max_width, 800.0);
928 assert_eq!(s.flow_direction, FlowDirection::Horizontal);
929 assert_eq!(s.alignment, TextAlignment::Left);
930 assert_eq!(s.line_spacing.line_height_multiplier, 1.0);
931 }
932
933 #[test]
934 fn text_style_builders() {
935 let s = TextStyle::default()
936 .with_alignment(TextAlignment::Center)
937 .with_font_size(24.0)
938 .with_max_width(400.0);
939 assert_eq!(s.alignment, TextAlignment::Center);
940 assert_eq!(s.font_size, 24.0);
941 assert_eq!(s.max_width, 400.0);
942 }
943
944 #[test]
945 fn shaped_glyph_default_is_notdef() {
946 let g = ShapedGlyph::default();
947 assert_eq!(g.gid, 0);
948 assert_eq!(g.x_advance, 0.0);
949 assert!(!g.is_whitespace);
950 assert!(!g.unsafe_to_break);
951 }
952
953 #[test]
954 fn glyph_metrics_default_is_zero() {
955 let m = GlyphMetrics::default();
956 assert_eq!(m.advance_x, 0.0);
957 assert_eq!(m.width, 0.0);
958 }
959
960 #[test]
961 fn writing_mode_flow_direction_mapping() {
962 assert_eq!(
963 WritingMode::HorizontalTb.flow_direction(),
964 FlowDirection::Horizontal
965 );
966 assert_eq!(
967 WritingMode::VerticalRl.flow_direction(),
968 FlowDirection::Vertical
969 );
970 assert_eq!(
971 WritingMode::VerticalLr.flow_direction(),
972 FlowDirection::Vertical
973 );
974 assert!(!WritingMode::HorizontalTb.is_vertical());
975 assert!(WritingMode::VerticalRl.is_vertical());
976 }
977
978 #[test]
979 fn line_spacing_resolve() {
980 let ls = LineSpacing {
981 leading: 2.0,
982 line_height_multiplier: 1.5,
983 };
984 assert!((ls.resolve(20.0) - 32.0).abs() < f32::EPSILON);
986 let def = LineSpacing::default();
987 assert!((def.resolve(20.0) - 20.0).abs() < f32::EPSILON);
988 }
989
990 #[test]
991 fn decoration_any_flag() {
992 let none = Decoration::default();
993 assert!(!none.any());
994 let under = Decoration {
995 underline: Some(DecorationLine {
996 position: -2.0,
997 thickness: 1.0,
998 color: Rgba8::BLACK,
999 }),
1000 ..Default::default()
1001 };
1002 assert!(under.any());
1003 }
1004
1005 #[test]
1006 fn glyph_cluster_advance_and_empty() {
1007 let empty = GlyphCluster {
1008 glyphs: vec![],
1009 source_start: 0,
1010 source_end: 0,
1011 };
1012 assert!(empty.is_empty());
1013 assert_eq!(empty.advance(), 0.0);
1014
1015 let cluster = GlyphCluster {
1016 glyphs: vec![
1017 ShapedGlyph {
1018 x_advance: 10.0,
1019 ..Default::default()
1020 },
1021 ShapedGlyph {
1022 x_advance: 5.0,
1023 ..Default::default()
1024 },
1025 ],
1026 source_start: 0,
1027 source_end: 3,
1028 };
1029 assert!(!cluster.is_empty());
1030 assert!((cluster.advance() - 15.0).abs() < f32::EPSILON);
1031 }
1032
1033 #[test]
1034 fn bitmap_and_color_bitmap_empty() {
1035 let bm = Bitmap {
1036 width: 0,
1037 height: 0,
1038 pixels: vec![],
1039 };
1040 assert!(bm.is_empty());
1041 let cbm = ColorBitmap {
1042 width: 2,
1043 height: 2,
1044 rgba: vec![0; 16],
1045 };
1046 assert!(!cbm.is_empty());
1047 }
1048
1049 #[test]
1050 fn render_output_variants_construct() {
1051 let g = RenderOutput::Greyscale(Bitmap {
1052 width: 1,
1053 height: 1,
1054 pixels: vec![255],
1055 });
1056 let c = RenderOutput::Color(ColorBitmap {
1057 width: 1,
1058 height: 1,
1059 rgba: vec![0, 0, 0, 255],
1060 });
1061 let s = RenderOutput::Sdf {
1062 width: 1,
1063 height: 1,
1064 data: vec![128],
1065 };
1066 let lcd = RenderOutput::Lcd(LcdBitmap::new(1, 1, vec![255, 0, 0]));
1067 let msdf = RenderOutput::Msdf {
1068 width: 1,
1069 height: 1,
1070 data: vec![100, 128, 200],
1071 };
1072 assert!(matches!(g, RenderOutput::Greyscale(_)));
1074 assert!(matches!(c, RenderOutput::Color(_)));
1075 assert!(matches!(s, RenderOutput::Sdf { .. }));
1076 assert!(matches!(lcd, RenderOutput::Lcd(_)));
1077 assert!(matches!(msdf, RenderOutput::Msdf { .. }));
1078 }
1079
1080 #[test]
1081 fn lcd_bitmap_new_constructor() {
1082 let bm = LcdBitmap::new(4, 2, vec![0u8; 4 * 2 * 3]);
1083 assert_eq!(bm.width, 4);
1084 assert_eq!(bm.height, 2);
1085 assert_eq!(bm.rgb.len(), 24);
1086 assert!(!bm.is_empty());
1087 }
1088
1089 #[test]
1090 fn lcd_bitmap_is_empty() {
1091 let empty_w = LcdBitmap {
1092 width: 0,
1093 height: 1,
1094 rgb: vec![],
1095 };
1096 assert!(empty_w.is_empty());
1097 let empty_h = LcdBitmap {
1098 width: 1,
1099 height: 0,
1100 rgb: vec![],
1101 };
1102 assert!(empty_h.is_empty());
1103 let empty_buf = LcdBitmap {
1104 width: 1,
1105 height: 1,
1106 rgb: vec![],
1107 };
1108 assert!(empty_buf.is_empty());
1109 }
1110
1111 #[test]
1112 fn msdf_variant_fields() {
1113 let msdf = RenderOutput::Msdf {
1114 width: 8,
1115 height: 8,
1116 data: vec![0u8; 8 * 8 * 3],
1117 };
1118 if let RenderOutput::Msdf {
1119 width,
1120 height,
1121 data,
1122 } = &msdf
1123 {
1124 assert_eq!(*width, 8);
1125 assert_eq!(*height, 8);
1126 assert_eq!(data.len(), 192);
1127 } else {
1128 panic!("expected Msdf variant");
1129 }
1130 }
1131
1132 #[test]
1133 fn positioned_glyph_carries_font_size() {
1134 let pg = PositionedGlyph {
1135 gid: 5,
1136 font_data: Arc::from(&[][..]),
1137 pos: (1.0, 2.0),
1138 font_size: 18.0,
1139 advance_x: 12.0,
1140 cluster: 0,
1141 };
1142 assert_eq!(pg.font_size, 18.0);
1143 }
1144
1145 #[test]
1146 fn text_run_construction() {
1147 let run = TextRun {
1148 text: "hi".to_string(),
1149 font_data: Arc::from(&[][..]),
1150 style: TextStyle::default(),
1151 decoration: Decoration::default(),
1152 };
1153 assert_eq!(run.text, "hi");
1154 assert!(!run.decoration.any());
1155 }
1156
1157 #[test]
1158 fn flow_direction_is_hashable() {
1159 use std::collections::HashSet;
1160 let mut set = HashSet::new();
1161 set.insert(FlowDirection::Horizontal);
1162 set.insert(FlowDirection::Vertical);
1163 set.insert(FlowDirection::Horizontal);
1164 assert_eq!(set.len(), 2);
1165 }
1166
1167 #[test]
1168 fn text_alignment_is_hashable() {
1169 use std::collections::HashMap;
1170 let mut map = HashMap::new();
1171 map.insert(TextAlignment::Left, 1);
1172 map.insert(TextAlignment::Center, 2);
1173 assert_eq!(map.get(&TextAlignment::Left), Some(&1));
1174 }
1175
1176 #[test]
1177 fn oxitext_error_display_all_variants() {
1178 assert_eq!(
1179 OxiTextError::Shaping("x".into()).to_string(),
1180 "shaping error: x"
1181 );
1182 assert_eq!(
1183 OxiTextError::Layout("x".into()).to_string(),
1184 "layout error: x"
1185 );
1186 assert_eq!(
1187 OxiTextError::Raster("x".into()).to_string(),
1188 "raster error: x"
1189 );
1190 assert_eq!(OxiTextError::FontNotFound.to_string(), "font not found");
1191 assert_eq!(OxiTextError::InvalidFont.to_string(), "invalid font");
1192 assert_eq!(OxiTextError::Other("x".into()).to_string(), "text error: x");
1193 }
1194
1195 #[test]
1198 fn test_flow_direction_equality() {
1199 assert_eq!(FlowDirection::Horizontal, FlowDirection::Horizontal);
1200 assert_ne!(FlowDirection::Horizontal, FlowDirection::Vertical);
1201 }
1202
1203 #[test]
1204 fn test_flow_direction_clone() {
1205 let a = FlowDirection::Vertical;
1206 #[allow(clippy::clone_on_copy)]
1207 let b = Clone::clone(&a);
1208 assert_eq!(a, b);
1209 }
1210
1211 #[test]
1212 fn test_flow_direction_debug() {
1213 let s = format!("{:?}", FlowDirection::Horizontal);
1214 assert!(s.contains("Horizontal"));
1215 }
1216
1217 #[test]
1218 fn test_text_alignment_ordering() {
1219 assert_eq!(TextAlignment::Left, TextAlignment::Left);
1221 assert_ne!(TextAlignment::Left, TextAlignment::Right);
1222 }
1223
1224 #[test]
1227 fn test_shaped_glyph_negative_offsets() {
1228 let g = ShapedGlyph {
1230 gid: 0x301, x_advance: 0.0, y_advance: 0.0,
1233 x_offset: -2.5, y_offset: -8.0, cluster: 0,
1236 is_whitespace: false,
1237 unsafe_to_break: true, };
1239 assert!(g.x_offset < 0.0);
1240 assert!(g.y_offset < 0.0);
1241 assert!(g.unsafe_to_break);
1242 assert_eq!(g.x_advance, 0.0);
1243 }
1244
1245 #[test]
1246 fn test_shaped_glyph_default_is_notdef() {
1247 let g = ShapedGlyph::default();
1248 assert_eq!(g.gid, 0);
1249 assert_eq!(g.x_advance, 0.0);
1250 assert!(!g.unsafe_to_break);
1251 }
1252
1253 #[test]
1256 fn test_error_display() {
1257 let e = OxiTextError::FontNotFound;
1258 let s = format!("{e}");
1259 assert!(!s.is_empty());
1260 }
1261
1262 #[test]
1263 fn test_error_invalid_font() {
1264 let e = OxiTextError::InvalidFont;
1265 assert_ne!(format!("{e}"), format!("{}", OxiTextError::FontNotFound));
1266 }
1267
1268 #[test]
1269 fn types_are_send_sync() {
1270 fn assert_send_sync<T: Send + Sync>() {}
1271 assert_send_sync::<ShapedGlyph>();
1272 assert_send_sync::<ShapedRun>();
1273 assert_send_sync::<PositionedGlyph>();
1274 assert_send_sync::<Bitmap>();
1275 assert_send_sync::<ColorBitmap>();
1276 assert_send_sync::<LcdBitmap>();
1277 assert_send_sync::<RenderOutput>();
1278 assert_send_sync::<TextStyle>();
1279 assert_send_sync::<ParagraphStyle>();
1280 assert_send_sync::<TextRun>();
1281 assert_send_sync::<GlyphCluster>();
1282 assert_send_sync::<GlyphMetrics>();
1283 }
1284
1285 #[test]
1286 fn render_output_into_bitmap_greyscale() {
1287 let bm = Bitmap {
1288 width: 4,
1289 height: 4,
1290 pixels: vec![255u8; 16],
1291 };
1292 let out = RenderOutput::Greyscale(bm.clone());
1293 let extracted: Option<Bitmap> = out.into();
1294 assert!(extracted.is_some());
1295 let extracted = extracted.expect("greyscale should yield Some(Bitmap)");
1296 assert_eq!(extracted.width, 4);
1297 assert_eq!(extracted.pixels.len(), 16);
1298 }
1299
1300 #[test]
1301 fn render_output_into_bitmap_non_greyscale_is_none() {
1302 let out = RenderOutput::Sdf {
1303 width: 4,
1304 height: 4,
1305 data: vec![128u8; 16],
1306 };
1307 let extracted: Option<Bitmap> = out.into();
1308 assert!(extracted.is_none());
1309
1310 let out2 = RenderOutput::Msdf {
1311 width: 4,
1312 height: 4,
1313 data: vec![100u8; 48],
1314 };
1315 let extracted2: Option<Bitmap> = out2.into();
1316 assert!(extracted2.is_none());
1317 }
1318
1319 #[cfg(feature = "serde")]
1320 #[test]
1321 fn serde_roundtrip_bitmap() {
1322 let bm = Bitmap {
1323 width: 2,
1324 height: 2,
1325 pixels: vec![0, 128, 200, 255],
1326 };
1327 let json = serde_json::to_string(&bm).expect("serialize Bitmap");
1328 let back: Bitmap = serde_json::from_str(&json).expect("deserialize Bitmap");
1329 assert_eq!(back.width, bm.width);
1330 assert_eq!(back.pixels, bm.pixels);
1331 }
1332
1333 #[test]
1334 fn test_decoration_rect_fields() {
1335 let r = DecorationRect {
1336 x: 1.0,
1337 y: 2.0,
1338 width: 10.0,
1339 height: 1.5,
1340 color: Rgba8 {
1341 r: 0,
1342 g: 0,
1343 b: 0,
1344 a: 255,
1345 },
1346 };
1347 assert_eq!(r.width, 10.0);
1348 assert_eq!(r.height, 1.5);
1349 assert_eq!(r.color.a, 255);
1350 }
1351
1352 #[test]
1353 fn test_text_decoration_variants() {
1354 let under = TextDecoration::Underline {
1355 color: Rgba8::BLACK,
1356 thickness: 1.0,
1357 offset: 2.0,
1358 };
1359 let over = TextDecoration::Overline {
1360 color: Rgba8::BLACK,
1361 thickness: 1.0,
1362 offset: 0.0,
1363 };
1364 let strike = TextDecoration::Strikethrough {
1365 color: Rgba8::BLACK,
1366 thickness: 1.5,
1367 };
1368 assert_ne!(under, over);
1369 assert_ne!(under, strike);
1370 let _copy = under;
1372 let _copy2 = over;
1373 }
1374
1375 #[cfg(feature = "serde")]
1376 #[test]
1377 fn serde_roundtrip_text_style() {
1378 let style = TextStyle {
1379 font_size: 24.0,
1380 max_width: 600.0,
1381 flow_direction: FlowDirection::Vertical,
1382 alignment: TextAlignment::Center,
1383 line_spacing: LineSpacing {
1384 leading: 2.0,
1385 line_height_multiplier: 1.5,
1386 },
1387 };
1388 let json = serde_json::to_string(&style).expect("serialize TextStyle");
1389 let back: TextStyle = serde_json::from_str(&json).expect("deserialize TextStyle");
1390 assert_eq!(back.font_size, 24.0);
1391 assert_eq!(back.alignment, TextAlignment::Center);
1392 assert_eq!(back.flow_direction, FlowDirection::Vertical);
1393 }
1394
1395 #[test]
1398 fn test_bitmap_invert_coverage() {
1399 let b = Bitmap {
1400 width: 2,
1401 height: 1,
1402 pixels: vec![0u8, 255],
1403 };
1404 let inv = b.invert_coverage();
1405 assert_eq!(inv.pixels[0], 255);
1406 assert_eq!(inv.pixels[1], 0);
1407 }
1408
1409 #[test]
1410 fn test_bitmap_threshold() {
1411 let b = Bitmap {
1412 width: 3,
1413 height: 1,
1414 pixels: vec![64u8, 128, 200],
1415 };
1416 let t = b.threshold(128);
1417 assert_eq!(t.pixels[0], 0);
1418 assert_eq!(t.pixels[1], 255);
1419 assert_eq!(t.pixels[2], 255);
1420 }
1421
1422 #[test]
1423 fn test_bitmap_tight_bounds_all_zero_returns_none() {
1424 let b = Bitmap {
1425 width: 4,
1426 height: 4,
1427 pixels: vec![0u8; 16],
1428 };
1429 assert!(b.tight_bounds().is_none());
1430 }
1431
1432 #[test]
1433 fn test_bitmap_tight_bounds_single_pixel() {
1434 let mut pixels = vec![0u8; 16];
1435 pixels[4 * 2 + 1] = 255; let b = Bitmap {
1437 width: 4,
1438 height: 4,
1439 pixels,
1440 };
1441 let bounds = b.tight_bounds().expect("should find pixel");
1442 assert_eq!(bounds, (1, 2, 1, 2));
1443 }
1444
1445 #[test]
1446 fn test_bitmap_crop() {
1447 let pixels = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
1448 let b = Bitmap {
1449 width: 4,
1450 height: 4,
1451 pixels,
1452 };
1453 let cropped = b.crop(1, 1, 2, 2);
1454 assert_eq!(cropped.width, 2);
1455 assert_eq!(cropped.height, 2);
1456 assert_eq!(cropped.pixels, vec![6u8, 7, 10, 11]);
1457 }
1458
1459 #[test]
1460 fn test_bitmap_invert_is_involution() {
1461 let b = Bitmap {
1462 width: 3,
1463 height: 1,
1464 pixels: vec![10u8, 128, 200],
1465 };
1466 let double_inv = b.invert_coverage().invert_coverage();
1467 assert_eq!(double_inv.pixels, b.pixels);
1468 }
1469
1470 #[test]
1471 fn test_bitmap_crop_out_of_bounds_fills_zero() {
1472 let b = Bitmap {
1473 width: 2,
1474 height: 2,
1475 pixels: vec![1u8, 2, 3, 4],
1476 };
1477 let cropped = b.crop(5, 5, 3, 3);
1479 assert_eq!(cropped.pixels, vec![0u8; 9]);
1480 }
1481
1482 #[test]
1483 fn test_std_feature_enabled_by_default() {
1484 #[cfg(feature = "std")]
1487 {
1488 let err: &dyn core::error::Error = &OxiTextError::InvalidFont;
1490 let _ = err.to_string();
1491 }
1492 }
1493
1494 #[test]
1495 fn test_vertical_position_effective_size() {
1496 let vp = VerticalPosition::Superscript {
1497 size_ratio: 0.6,
1498 baseline_rise: 4.0,
1499 };
1500 assert!((vp.effective_size(16.0) - 9.6).abs() < 0.001);
1501 }
1502
1503 #[test]
1504 fn test_vertical_position_baseline_adjustment() {
1505 let sub = VerticalPosition::Subscript {
1506 size_ratio: 0.6,
1507 baseline_drop: 3.0,
1508 };
1509 assert_eq!(sub.baseline_adjustment(16.0), -3.0);
1510 let norm = VerticalPosition::Normal;
1511 assert_eq!(norm.baseline_adjustment(16.0), 0.0);
1512 }
1513}