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#[derive(Debug, Clone)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct ShapedGlyph {
25 pub gid: u16,
27 pub x_advance: f32,
29 pub y_advance: f32,
31 pub x_offset: f32,
33 pub y_offset: f32,
35 pub cluster: u32,
37 pub is_whitespace: bool,
42 pub unsafe_to_break: bool,
46}
47
48impl Default for ShapedGlyph {
49 fn default() -> Self {
51 Self {
52 gid: 0,
53 x_advance: 0.0,
54 y_advance: 0.0,
55 x_offset: 0.0,
56 y_offset: 0.0,
57 cluster: 0,
58 is_whitespace: false,
59 unsafe_to_break: false,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct FontVerticalMetrics {
77 pub units_per_em: u16,
79 pub ascender: i16,
81 pub descender: i16,
83 pub line_gap: i16,
85}
86
87impl FontVerticalMetrics {
88 pub fn ascent_px(&self, font_size_px: f32) -> f32 {
90 if self.units_per_em == 0 {
91 return font_size_px * 0.8;
92 }
93 self.ascender as f32 * font_size_px / self.units_per_em as f32
94 }
95
96 pub fn descent_px(&self, font_size_px: f32) -> f32 {
98 if self.units_per_em == 0 {
99 return font_size_px * 0.2;
100 }
101 (-(self.descender as f32)) * font_size_px / self.units_per_em as f32
102 }
103
104 pub fn line_gap_px(&self, font_size_px: f32) -> f32 {
106 if self.units_per_em == 0 {
107 return font_size_px * 0.4;
108 }
109 self.line_gap as f32 * font_size_px / self.units_per_em as f32
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122pub struct GlyphMetrics {
123 pub bearing_x: f32,
125 pub bearing_y: f32,
127 pub advance_x: f32,
129 pub advance_y: f32,
131 pub width: f32,
133 pub height: f32,
135}
136
137impl Default for GlyphMetrics {
138 fn default() -> Self {
139 Self {
140 bearing_x: 0.0,
141 bearing_y: 0.0,
142 advance_x: 0.0,
143 advance_y: 0.0,
144 width: 0.0,
145 height: 0.0,
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
157pub struct GlyphCluster {
158 pub glyphs: Vec<ShapedGlyph>,
160 pub source_start: u32,
162 pub source_end: u32,
164}
165
166impl GlyphCluster {
167 pub fn advance(&self) -> f32 {
169 self.glyphs.iter().map(|g| g.x_advance).sum()
170 }
171
172 pub fn is_empty(&self) -> bool {
174 self.glyphs.is_empty()
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct ShapedRun {
181 pub glyphs: SmallVec<[ShapedGlyph; 8]>,
186 pub font_data: Arc<[u8]>,
188}
189
190#[derive(Debug, Clone)]
192pub struct PositionedGlyph {
193 pub gid: u16,
195 pub font_data: Arc<[u8]>,
197 pub pos: (f32, f32),
199 pub font_size: f32,
205 pub advance_x: f32,
210 pub cluster: u32,
216}
217
218#[derive(Debug, Clone)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
221pub struct Bitmap {
222 pub width: u32,
224 pub height: u32,
226 pub pixels: Vec<u8>,
228}
229
230impl Bitmap {
231 pub fn is_empty(&self) -> bool {
234 self.width == 0 || self.height == 0 || self.pixels.is_empty()
235 }
236
237 pub fn invert_coverage(&self) -> Self {
243 Bitmap {
244 width: self.width,
245 height: self.height,
246 pixels: self.pixels.iter().map(|&v| 255 - v).collect(),
247 }
248 }
249
250 pub fn threshold(&self, threshold: u8) -> Self {
256 Bitmap {
257 width: self.width,
258 height: self.height,
259 pixels: self
260 .pixels
261 .iter()
262 .map(|&v| if v >= threshold { 255 } else { 0 })
263 .collect(),
264 }
265 }
266
267 pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> Self {
270 let mut pixels = vec![0u8; (width * height) as usize];
271 for row in 0..height {
272 for col in 0..width {
273 let src_x = x + col;
274 let src_y = y + row;
275 if src_x < self.width && src_y < self.height {
276 let src_idx = (src_y * self.width + src_x) as usize;
277 let dst_idx = (row * width + col) as usize;
278 pixels[dst_idx] = self.pixels[src_idx];
279 }
280 }
281 }
282 Bitmap {
283 width,
284 height,
285 pixels,
286 }
287 }
288
289 pub fn tight_bounds(&self) -> Option<(u32, u32, u32, u32)> {
295 let mut x_min = self.width;
296 let mut y_min = self.height;
297 let mut x_max = 0u32;
298 let mut y_max = 0u32;
299
300 for row in 0..self.height {
301 for col in 0..self.width {
302 if self.pixels[(row * self.width + col) as usize] > 0 {
303 x_min = x_min.min(col);
304 y_min = y_min.min(row);
305 x_max = x_max.max(col);
306 y_max = y_max.max(row);
307 }
308 }
309 }
310
311 if x_min > x_max {
312 None
313 } else {
314 Some((x_min, y_min, x_max, y_max))
315 }
316 }
317}
318
319#[derive(Debug, Clone)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub struct ColorBitmap {
326 pub width: u32,
328 pub height: u32,
330 pub rgba: Vec<u8>,
332}
333
334impl ColorBitmap {
335 pub fn is_empty(&self) -> bool {
337 self.width == 0 || self.height == 0 || self.rgba.is_empty()
338 }
339}
340
341#[derive(Debug, Clone)]
350#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
351pub struct LcdBitmap {
352 pub width: u32,
354 pub height: u32,
356 pub rgb: Vec<u8>,
358}
359
360impl LcdBitmap {
361 pub fn new(width: u32, height: u32, rgb: Vec<u8>) -> Self {
368 debug_assert_eq!(
369 rgb.len(),
370 (width as usize) * (height as usize) * 3,
371 "LcdBitmap: rgb buffer length must equal width * height * 3"
372 );
373 Self { width, height, rgb }
374 }
375
376 pub fn is_empty(&self) -> bool {
378 self.width == 0 || self.height == 0 || self.rgb.is_empty()
379 }
380}
381
382#[derive(Debug, Clone)]
388#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
389pub enum RenderOutput {
390 Greyscale(Bitmap),
392 Color(ColorBitmap),
394 Sdf {
396 width: u32,
398 height: u32,
400 data: Vec<u8>,
402 },
403 Lcd(LcdBitmap),
408 Msdf {
414 width: u32,
416 height: u32,
418 data: Vec<u8>,
420 },
421}
422
423#[derive(Debug, Clone)]
425#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
426pub struct LayoutConstraints {
427 pub max_width: f32,
429 pub font_size: f32,
431}
432
433impl Default for LayoutConstraints {
434 fn default() -> Self {
435 Self {
436 max_width: 800.0,
437 font_size: 16.0,
438 }
439 }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
448#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
449pub enum FlowDirection {
450 #[default]
452 Horizontal,
453 Vertical,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
461#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
462pub enum TextAlignment {
463 #[default]
465 Left,
466 Right,
468 Center,
470 Justify,
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
482#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
483pub enum WritingMode {
484 #[default]
486 HorizontalTb,
487 VerticalRl,
489 VerticalLr,
491}
492
493impl WritingMode {
494 pub fn flow_direction(self) -> FlowDirection {
496 match self {
497 WritingMode::HorizontalTb => FlowDirection::Horizontal,
498 WritingMode::VerticalRl | WritingMode::VerticalLr => FlowDirection::Vertical,
499 }
500 }
501
502 pub fn is_vertical(self) -> bool {
504 !matches!(self, WritingMode::HorizontalTb)
505 }
506}
507
508#[derive(Debug, Clone, Copy, PartialEq)]
514#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
515pub struct LineSpacing {
516 pub leading: f32,
518 pub line_height_multiplier: f32,
520}
521
522impl Default for LineSpacing {
523 fn default() -> Self {
524 Self {
525 leading: 0.0,
526 line_height_multiplier: 1.0,
527 }
528 }
529}
530
531impl LineSpacing {
532 pub fn resolve(&self, natural_line_height: f32) -> f32 {
534 natural_line_height * self.line_height_multiplier + self.leading
535 }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
540#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
541pub struct Rgba8 {
542 pub r: u8,
544 pub g: u8,
546 pub b: u8,
548 pub a: u8,
550}
551
552impl Rgba8 {
553 pub const BLACK: Rgba8 = Rgba8 {
555 r: 0,
556 g: 0,
557 b: 0,
558 a: 255,
559 };
560 pub const TRANSPARENT: Rgba8 = Rgba8 {
562 r: 0,
563 g: 0,
564 b: 0,
565 a: 0,
566 };
567
568 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
570 Self { r, g, b, a }
571 }
572}
573
574impl Default for Rgba8 {
575 fn default() -> Self {
576 Rgba8::BLACK
577 }
578}
579
580#[derive(Debug, Clone, Copy, PartialEq)]
584#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
585pub struct DecorationLine {
586 pub position: f32,
590 pub thickness: f32,
592 pub color: Rgba8,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq)]
602#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
603pub enum TextDecoration {
604 Underline {
606 color: Rgba8,
608 thickness: f32,
610 offset: f32,
612 },
613 Overline {
615 color: Rgba8,
617 thickness: f32,
619 offset: f32,
622 },
623 Strikethrough {
626 color: Rgba8,
628 thickness: f32,
630 },
631}
632
633#[derive(Debug, Clone, Copy, PartialEq)]
641#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
642pub struct DecorationRect {
643 pub x: f32,
645 pub y: f32,
647 pub width: f32,
649 pub height: f32,
651 pub color: Rgba8,
653}
654
655#[derive(Debug, Clone, Copy, PartialEq, Default)]
660#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
661pub struct Decoration {
662 pub underline: Option<DecorationLine>,
664 pub overline: Option<DecorationLine>,
666 pub strikethrough: Option<DecorationLine>,
668}
669
670impl Decoration {
671 pub fn any(&self) -> bool {
673 self.underline.is_some() || self.overline.is_some() || self.strikethrough.is_some()
674 }
675}
676
677#[derive(Debug, Clone)]
679#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
680pub struct TextStyle {
681 pub font_size: f32,
683 pub max_width: f32,
685 pub flow_direction: FlowDirection,
687 pub alignment: TextAlignment,
689 pub line_spacing: LineSpacing,
691}
692
693impl Default for TextStyle {
694 fn default() -> Self {
695 Self {
696 font_size: 16.0,
697 max_width: 800.0,
698 flow_direction: FlowDirection::Horizontal,
699 alignment: TextAlignment::Left,
700 line_spacing: LineSpacing::default(),
701 }
702 }
703}
704
705impl TextStyle {
706 pub fn with_alignment(mut self, alignment: TextAlignment) -> Self {
708 self.alignment = alignment;
709 self
710 }
711
712 pub fn with_font_size(mut self, font_size: f32) -> Self {
714 self.font_size = font_size;
715 self
716 }
717
718 pub fn with_max_width(mut self, max_width: f32) -> Self {
720 self.max_width = max_width;
721 self
722 }
723
724 pub fn with_flow_direction(mut self, flow_direction: FlowDirection) -> Self {
726 self.flow_direction = flow_direction;
727 self
728 }
729}
730
731#[derive(Debug, Clone)]
736#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
737pub struct ParagraphStyle {
738 pub alignment: TextAlignment,
740 pub indent: f32,
742 pub spacing_before: f32,
744 pub spacing_after: f32,
746 pub direction: FlowDirection,
748 pub line_spacing: LineSpacing,
750}
751
752impl Default for ParagraphStyle {
753 fn default() -> Self {
754 Self {
755 alignment: TextAlignment::Left,
756 indent: 0.0,
757 spacing_before: 0.0,
758 spacing_after: 0.0,
759 direction: FlowDirection::Horizontal,
760 line_spacing: LineSpacing::default(),
761 }
762 }
763}
764
765#[derive(Debug, Clone)]
771pub struct TextRun {
772 pub text: String,
774 pub font_data: Arc<[u8]>,
776 pub style: TextStyle,
778 pub decoration: Decoration,
780}
781
782#[derive(Debug, Clone, PartialEq)]
785pub struct InlineObject {
786 pub id: u64,
788 pub width: f32,
790 pub height: f32,
792 pub baseline_offset: f32,
794 pub advance: f32,
796}
797
798#[derive(Debug, Clone, PartialEq)]
800pub struct PositionedInlineObject {
801 pub object: InlineObject,
803 pub x: f32,
805 pub y: f32,
807 pub line: usize,
809}
810
811#[derive(Debug, Clone, Copy, PartialEq, Default)]
813pub enum VerticalPosition {
814 #[default]
816 Normal,
817 Superscript {
819 size_ratio: f32,
821 baseline_rise: f32,
823 },
824 Subscript {
826 size_ratio: f32,
828 baseline_drop: f32,
830 },
831}
832
833impl VerticalPosition {
834 pub fn effective_size(&self, base_px: f32) -> f32 {
836 match self {
837 Self::Normal => base_px,
838 Self::Superscript { size_ratio, .. } => base_px * size_ratio,
839 Self::Subscript { size_ratio, .. } => base_px * size_ratio,
840 }
841 }
842
843 pub fn baseline_adjustment(&self, _base_px: f32) -> f32 {
845 match self {
846 Self::Normal => 0.0,
847 Self::Superscript { baseline_rise, .. } => *baseline_rise,
848 Self::Subscript { baseline_drop, .. } => -*baseline_drop,
849 }
850 }
851}
852
853#[derive(Debug)]
855pub enum OxiTextError {
856 Shaping(String),
858 Layout(String),
860 Raster(String),
862 FontNotFound,
864 InvalidFont,
866 Other(String),
868}
869
870impl core::fmt::Display for OxiTextError {
871 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
872 match self {
873 OxiTextError::Shaping(s) => write!(f, "shaping error: {s}"),
874 OxiTextError::Layout(s) => write!(f, "layout error: {s}"),
875 OxiTextError::Raster(s) => write!(f, "raster error: {s}"),
876 OxiTextError::FontNotFound => write!(f, "font not found"),
877 OxiTextError::InvalidFont => write!(f, "invalid font"),
878 OxiTextError::Other(s) => write!(f, "text error: {s}"),
879 }
880 }
881}
882
883impl core::error::Error for OxiTextError {}
884
885impl RenderOutput {
886 pub fn into_bitmap(self) -> Option<Bitmap> {
889 match self {
890 RenderOutput::Greyscale(b) => Some(b),
891 _ => None,
892 }
893 }
894}
895
896impl From<RenderOutput> for Option<Bitmap> {
897 fn from(output: RenderOutput) -> Self {
900 output.into_bitmap()
901 }
902}
903
904#[cfg(all(test, feature = "std"))]
905mod tests {
906 use super::*;
907 use std::sync::Arc;
908
909 #[test]
910 fn layout_constraints_default_values() {
911 let c = LayoutConstraints::default();
912 assert_eq!(c.max_width, 800.0);
913 assert_eq!(c.font_size, 16.0);
914 }
915
916 #[test]
917 fn text_style_default_values() {
918 let s = TextStyle::default();
919 assert_eq!(s.font_size, 16.0);
920 assert_eq!(s.max_width, 800.0);
921 assert_eq!(s.flow_direction, FlowDirection::Horizontal);
922 assert_eq!(s.alignment, TextAlignment::Left);
923 assert_eq!(s.line_spacing.line_height_multiplier, 1.0);
924 }
925
926 #[test]
927 fn text_style_builders() {
928 let s = TextStyle::default()
929 .with_alignment(TextAlignment::Center)
930 .with_font_size(24.0)
931 .with_max_width(400.0);
932 assert_eq!(s.alignment, TextAlignment::Center);
933 assert_eq!(s.font_size, 24.0);
934 assert_eq!(s.max_width, 400.0);
935 }
936
937 #[test]
938 fn shaped_glyph_default_is_notdef() {
939 let g = ShapedGlyph::default();
940 assert_eq!(g.gid, 0);
941 assert_eq!(g.x_advance, 0.0);
942 assert!(!g.is_whitespace);
943 assert!(!g.unsafe_to_break);
944 }
945
946 #[test]
947 fn glyph_metrics_default_is_zero() {
948 let m = GlyphMetrics::default();
949 assert_eq!(m.advance_x, 0.0);
950 assert_eq!(m.width, 0.0);
951 }
952
953 #[test]
954 fn writing_mode_flow_direction_mapping() {
955 assert_eq!(
956 WritingMode::HorizontalTb.flow_direction(),
957 FlowDirection::Horizontal
958 );
959 assert_eq!(
960 WritingMode::VerticalRl.flow_direction(),
961 FlowDirection::Vertical
962 );
963 assert_eq!(
964 WritingMode::VerticalLr.flow_direction(),
965 FlowDirection::Vertical
966 );
967 assert!(!WritingMode::HorizontalTb.is_vertical());
968 assert!(WritingMode::VerticalRl.is_vertical());
969 }
970
971 #[test]
972 fn line_spacing_resolve() {
973 let ls = LineSpacing {
974 leading: 2.0,
975 line_height_multiplier: 1.5,
976 };
977 assert!((ls.resolve(20.0) - 32.0).abs() < f32::EPSILON);
979 let def = LineSpacing::default();
980 assert!((def.resolve(20.0) - 20.0).abs() < f32::EPSILON);
981 }
982
983 #[test]
984 fn decoration_any_flag() {
985 let none = Decoration::default();
986 assert!(!none.any());
987 let under = Decoration {
988 underline: Some(DecorationLine {
989 position: -2.0,
990 thickness: 1.0,
991 color: Rgba8::BLACK,
992 }),
993 ..Default::default()
994 };
995 assert!(under.any());
996 }
997
998 #[test]
999 fn glyph_cluster_advance_and_empty() {
1000 let empty = GlyphCluster {
1001 glyphs: vec![],
1002 source_start: 0,
1003 source_end: 0,
1004 };
1005 assert!(empty.is_empty());
1006 assert_eq!(empty.advance(), 0.0);
1007
1008 let cluster = GlyphCluster {
1009 glyphs: vec![
1010 ShapedGlyph {
1011 x_advance: 10.0,
1012 ..Default::default()
1013 },
1014 ShapedGlyph {
1015 x_advance: 5.0,
1016 ..Default::default()
1017 },
1018 ],
1019 source_start: 0,
1020 source_end: 3,
1021 };
1022 assert!(!cluster.is_empty());
1023 assert!((cluster.advance() - 15.0).abs() < f32::EPSILON);
1024 }
1025
1026 #[test]
1027 fn bitmap_and_color_bitmap_empty() {
1028 let bm = Bitmap {
1029 width: 0,
1030 height: 0,
1031 pixels: vec![],
1032 };
1033 assert!(bm.is_empty());
1034 let cbm = ColorBitmap {
1035 width: 2,
1036 height: 2,
1037 rgba: vec![0; 16],
1038 };
1039 assert!(!cbm.is_empty());
1040 }
1041
1042 #[test]
1043 fn render_output_variants_construct() {
1044 let g = RenderOutput::Greyscale(Bitmap {
1045 width: 1,
1046 height: 1,
1047 pixels: vec![255],
1048 });
1049 let c = RenderOutput::Color(ColorBitmap {
1050 width: 1,
1051 height: 1,
1052 rgba: vec![0, 0, 0, 255],
1053 });
1054 let s = RenderOutput::Sdf {
1055 width: 1,
1056 height: 1,
1057 data: vec![128],
1058 };
1059 let lcd = RenderOutput::Lcd(LcdBitmap::new(1, 1, vec![255, 0, 0]));
1060 let msdf = RenderOutput::Msdf {
1061 width: 1,
1062 height: 1,
1063 data: vec![100, 128, 200],
1064 };
1065 assert!(matches!(g, RenderOutput::Greyscale(_)));
1067 assert!(matches!(c, RenderOutput::Color(_)));
1068 assert!(matches!(s, RenderOutput::Sdf { .. }));
1069 assert!(matches!(lcd, RenderOutput::Lcd(_)));
1070 assert!(matches!(msdf, RenderOutput::Msdf { .. }));
1071 }
1072
1073 #[test]
1074 fn lcd_bitmap_new_constructor() {
1075 let bm = LcdBitmap::new(4, 2, vec![0u8; 4 * 2 * 3]);
1076 assert_eq!(bm.width, 4);
1077 assert_eq!(bm.height, 2);
1078 assert_eq!(bm.rgb.len(), 24);
1079 assert!(!bm.is_empty());
1080 }
1081
1082 #[test]
1083 fn lcd_bitmap_is_empty() {
1084 let empty_w = LcdBitmap {
1085 width: 0,
1086 height: 1,
1087 rgb: vec![],
1088 };
1089 assert!(empty_w.is_empty());
1090 let empty_h = LcdBitmap {
1091 width: 1,
1092 height: 0,
1093 rgb: vec![],
1094 };
1095 assert!(empty_h.is_empty());
1096 let empty_buf = LcdBitmap {
1097 width: 1,
1098 height: 1,
1099 rgb: vec![],
1100 };
1101 assert!(empty_buf.is_empty());
1102 }
1103
1104 #[test]
1105 fn msdf_variant_fields() {
1106 let msdf = RenderOutput::Msdf {
1107 width: 8,
1108 height: 8,
1109 data: vec![0u8; 8 * 8 * 3],
1110 };
1111 if let RenderOutput::Msdf {
1112 width,
1113 height,
1114 data,
1115 } = &msdf
1116 {
1117 assert_eq!(*width, 8);
1118 assert_eq!(*height, 8);
1119 assert_eq!(data.len(), 192);
1120 } else {
1121 panic!("expected Msdf variant");
1122 }
1123 }
1124
1125 #[test]
1126 fn positioned_glyph_carries_font_size() {
1127 let pg = PositionedGlyph {
1128 gid: 5,
1129 font_data: Arc::from(&[][..]),
1130 pos: (1.0, 2.0),
1131 font_size: 18.0,
1132 advance_x: 12.0,
1133 cluster: 0,
1134 };
1135 assert_eq!(pg.font_size, 18.0);
1136 }
1137
1138 #[test]
1139 fn text_run_construction() {
1140 let run = TextRun {
1141 text: "hi".to_string(),
1142 font_data: Arc::from(&[][..]),
1143 style: TextStyle::default(),
1144 decoration: Decoration::default(),
1145 };
1146 assert_eq!(run.text, "hi");
1147 assert!(!run.decoration.any());
1148 }
1149
1150 #[test]
1151 fn flow_direction_is_hashable() {
1152 use std::collections::HashSet;
1153 let mut set = HashSet::new();
1154 set.insert(FlowDirection::Horizontal);
1155 set.insert(FlowDirection::Vertical);
1156 set.insert(FlowDirection::Horizontal);
1157 assert_eq!(set.len(), 2);
1158 }
1159
1160 #[test]
1161 fn text_alignment_is_hashable() {
1162 use std::collections::HashMap;
1163 let mut map = HashMap::new();
1164 map.insert(TextAlignment::Left, 1);
1165 map.insert(TextAlignment::Center, 2);
1166 assert_eq!(map.get(&TextAlignment::Left), Some(&1));
1167 }
1168
1169 #[test]
1170 fn oxitext_error_display_all_variants() {
1171 assert_eq!(
1172 OxiTextError::Shaping("x".into()).to_string(),
1173 "shaping error: x"
1174 );
1175 assert_eq!(
1176 OxiTextError::Layout("x".into()).to_string(),
1177 "layout error: x"
1178 );
1179 assert_eq!(
1180 OxiTextError::Raster("x".into()).to_string(),
1181 "raster error: x"
1182 );
1183 assert_eq!(OxiTextError::FontNotFound.to_string(), "font not found");
1184 assert_eq!(OxiTextError::InvalidFont.to_string(), "invalid font");
1185 assert_eq!(OxiTextError::Other("x".into()).to_string(), "text error: x");
1186 }
1187
1188 #[test]
1191 fn test_flow_direction_equality() {
1192 assert_eq!(FlowDirection::Horizontal, FlowDirection::Horizontal);
1193 assert_ne!(FlowDirection::Horizontal, FlowDirection::Vertical);
1194 }
1195
1196 #[test]
1197 fn test_flow_direction_clone() {
1198 let a = FlowDirection::Vertical;
1199 #[allow(clippy::clone_on_copy)]
1200 let b = Clone::clone(&a);
1201 assert_eq!(a, b);
1202 }
1203
1204 #[test]
1205 fn test_flow_direction_debug() {
1206 let s = format!("{:?}", FlowDirection::Horizontal);
1207 assert!(s.contains("Horizontal"));
1208 }
1209
1210 #[test]
1211 fn test_text_alignment_ordering() {
1212 assert_eq!(TextAlignment::Left, TextAlignment::Left);
1214 assert_ne!(TextAlignment::Left, TextAlignment::Right);
1215 }
1216
1217 #[test]
1220 fn test_shaped_glyph_negative_offsets() {
1221 let g = ShapedGlyph {
1223 gid: 0x301, x_advance: 0.0, y_advance: 0.0,
1226 x_offset: -2.5, y_offset: -8.0, cluster: 0,
1229 is_whitespace: false,
1230 unsafe_to_break: true, };
1232 assert!(g.x_offset < 0.0);
1233 assert!(g.y_offset < 0.0);
1234 assert!(g.unsafe_to_break);
1235 assert_eq!(g.x_advance, 0.0);
1236 }
1237
1238 #[test]
1239 fn test_shaped_glyph_default_is_notdef() {
1240 let g = ShapedGlyph::default();
1241 assert_eq!(g.gid, 0);
1242 assert_eq!(g.x_advance, 0.0);
1243 assert!(!g.unsafe_to_break);
1244 }
1245
1246 #[test]
1249 fn test_error_display() {
1250 let e = OxiTextError::FontNotFound;
1251 let s = format!("{e}");
1252 assert!(!s.is_empty());
1253 }
1254
1255 #[test]
1256 fn test_error_invalid_font() {
1257 let e = OxiTextError::InvalidFont;
1258 assert_ne!(format!("{e}"), format!("{}", OxiTextError::FontNotFound));
1259 }
1260
1261 #[test]
1262 fn types_are_send_sync() {
1263 fn assert_send_sync<T: Send + Sync>() {}
1264 assert_send_sync::<ShapedGlyph>();
1265 assert_send_sync::<ShapedRun>();
1266 assert_send_sync::<PositionedGlyph>();
1267 assert_send_sync::<Bitmap>();
1268 assert_send_sync::<ColorBitmap>();
1269 assert_send_sync::<LcdBitmap>();
1270 assert_send_sync::<RenderOutput>();
1271 assert_send_sync::<TextStyle>();
1272 assert_send_sync::<ParagraphStyle>();
1273 assert_send_sync::<TextRun>();
1274 assert_send_sync::<GlyphCluster>();
1275 assert_send_sync::<GlyphMetrics>();
1276 }
1277
1278 #[test]
1279 fn render_output_into_bitmap_greyscale() {
1280 let bm = Bitmap {
1281 width: 4,
1282 height: 4,
1283 pixels: vec![255u8; 16],
1284 };
1285 let out = RenderOutput::Greyscale(bm.clone());
1286 let extracted: Option<Bitmap> = out.into();
1287 assert!(extracted.is_some());
1288 let extracted = extracted.expect("greyscale should yield Some(Bitmap)");
1289 assert_eq!(extracted.width, 4);
1290 assert_eq!(extracted.pixels.len(), 16);
1291 }
1292
1293 #[test]
1294 fn render_output_into_bitmap_non_greyscale_is_none() {
1295 let out = RenderOutput::Sdf {
1296 width: 4,
1297 height: 4,
1298 data: vec![128u8; 16],
1299 };
1300 let extracted: Option<Bitmap> = out.into();
1301 assert!(extracted.is_none());
1302
1303 let out2 = RenderOutput::Msdf {
1304 width: 4,
1305 height: 4,
1306 data: vec![100u8; 48],
1307 };
1308 let extracted2: Option<Bitmap> = out2.into();
1309 assert!(extracted2.is_none());
1310 }
1311
1312 #[cfg(feature = "serde")]
1313 #[test]
1314 fn serde_roundtrip_bitmap() {
1315 let bm = Bitmap {
1316 width: 2,
1317 height: 2,
1318 pixels: vec![0, 128, 200, 255],
1319 };
1320 let json = serde_json::to_string(&bm).expect("serialize Bitmap");
1321 let back: Bitmap = serde_json::from_str(&json).expect("deserialize Bitmap");
1322 assert_eq!(back.width, bm.width);
1323 assert_eq!(back.pixels, bm.pixels);
1324 }
1325
1326 #[test]
1327 fn test_decoration_rect_fields() {
1328 let r = DecorationRect {
1329 x: 1.0,
1330 y: 2.0,
1331 width: 10.0,
1332 height: 1.5,
1333 color: Rgba8 {
1334 r: 0,
1335 g: 0,
1336 b: 0,
1337 a: 255,
1338 },
1339 };
1340 assert_eq!(r.width, 10.0);
1341 assert_eq!(r.height, 1.5);
1342 assert_eq!(r.color.a, 255);
1343 }
1344
1345 #[test]
1346 fn test_text_decoration_variants() {
1347 let under = TextDecoration::Underline {
1348 color: Rgba8::BLACK,
1349 thickness: 1.0,
1350 offset: 2.0,
1351 };
1352 let over = TextDecoration::Overline {
1353 color: Rgba8::BLACK,
1354 thickness: 1.0,
1355 offset: 0.0,
1356 };
1357 let strike = TextDecoration::Strikethrough {
1358 color: Rgba8::BLACK,
1359 thickness: 1.5,
1360 };
1361 assert_ne!(under, over);
1362 assert_ne!(under, strike);
1363 let _copy = under;
1365 let _copy2 = over;
1366 }
1367
1368 #[cfg(feature = "serde")]
1369 #[test]
1370 fn serde_roundtrip_text_style() {
1371 let style = TextStyle {
1372 font_size: 24.0,
1373 max_width: 600.0,
1374 flow_direction: FlowDirection::Vertical,
1375 alignment: TextAlignment::Center,
1376 line_spacing: LineSpacing {
1377 leading: 2.0,
1378 line_height_multiplier: 1.5,
1379 },
1380 };
1381 let json = serde_json::to_string(&style).expect("serialize TextStyle");
1382 let back: TextStyle = serde_json::from_str(&json).expect("deserialize TextStyle");
1383 assert_eq!(back.font_size, 24.0);
1384 assert_eq!(back.alignment, TextAlignment::Center);
1385 assert_eq!(back.flow_direction, FlowDirection::Vertical);
1386 }
1387
1388 #[test]
1391 fn test_bitmap_invert_coverage() {
1392 let b = Bitmap {
1393 width: 2,
1394 height: 1,
1395 pixels: vec![0u8, 255],
1396 };
1397 let inv = b.invert_coverage();
1398 assert_eq!(inv.pixels[0], 255);
1399 assert_eq!(inv.pixels[1], 0);
1400 }
1401
1402 #[test]
1403 fn test_bitmap_threshold() {
1404 let b = Bitmap {
1405 width: 3,
1406 height: 1,
1407 pixels: vec![64u8, 128, 200],
1408 };
1409 let t = b.threshold(128);
1410 assert_eq!(t.pixels[0], 0);
1411 assert_eq!(t.pixels[1], 255);
1412 assert_eq!(t.pixels[2], 255);
1413 }
1414
1415 #[test]
1416 fn test_bitmap_tight_bounds_all_zero_returns_none() {
1417 let b = Bitmap {
1418 width: 4,
1419 height: 4,
1420 pixels: vec![0u8; 16],
1421 };
1422 assert!(b.tight_bounds().is_none());
1423 }
1424
1425 #[test]
1426 fn test_bitmap_tight_bounds_single_pixel() {
1427 let mut pixels = vec![0u8; 16];
1428 pixels[4 * 2 + 1] = 255; let b = Bitmap {
1430 width: 4,
1431 height: 4,
1432 pixels,
1433 };
1434 let bounds = b.tight_bounds().expect("should find pixel");
1435 assert_eq!(bounds, (1, 2, 1, 2));
1436 }
1437
1438 #[test]
1439 fn test_bitmap_crop() {
1440 let pixels = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
1441 let b = Bitmap {
1442 width: 4,
1443 height: 4,
1444 pixels,
1445 };
1446 let cropped = b.crop(1, 1, 2, 2);
1447 assert_eq!(cropped.width, 2);
1448 assert_eq!(cropped.height, 2);
1449 assert_eq!(cropped.pixels, vec![6u8, 7, 10, 11]);
1450 }
1451
1452 #[test]
1453 fn test_bitmap_invert_is_involution() {
1454 let b = Bitmap {
1455 width: 3,
1456 height: 1,
1457 pixels: vec![10u8, 128, 200],
1458 };
1459 let double_inv = b.invert_coverage().invert_coverage();
1460 assert_eq!(double_inv.pixels, b.pixels);
1461 }
1462
1463 #[test]
1464 fn test_bitmap_crop_out_of_bounds_fills_zero() {
1465 let b = Bitmap {
1466 width: 2,
1467 height: 2,
1468 pixels: vec![1u8, 2, 3, 4],
1469 };
1470 let cropped = b.crop(5, 5, 3, 3);
1472 assert_eq!(cropped.pixels, vec![0u8; 9]);
1473 }
1474
1475 #[test]
1476 fn test_std_feature_enabled_by_default() {
1477 #[cfg(feature = "std")]
1480 {
1481 let err: &dyn core::error::Error = &OxiTextError::InvalidFont;
1483 let _ = err.to_string();
1484 }
1485 }
1486
1487 #[test]
1488 fn test_vertical_position_effective_size() {
1489 let vp = VerticalPosition::Superscript {
1490 size_ratio: 0.6,
1491 baseline_rise: 4.0,
1492 };
1493 assert!((vp.effective_size(16.0) - 9.6).abs() < 0.001);
1494 }
1495
1496 #[test]
1497 fn test_vertical_position_baseline_adjustment() {
1498 let sub = VerticalPosition::Subscript {
1499 size_ratio: 0.6,
1500 baseline_drop: 3.0,
1501 };
1502 assert_eq!(sub.baseline_adjustment(16.0), -3.0);
1503 let norm = VerticalPosition::Normal;
1504 assert_eq!(norm.baseline_adjustment(16.0), 0.0);
1505 }
1506}