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#[cfg(feature = "png-decode")]
35pub mod png_decode;
36
37#[derive(Debug, Clone)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct ShapedGlyph {
41 pub gid: u16,
43 pub x_advance: f32,
45 pub y_advance: f32,
47 pub x_offset: f32,
49 pub y_offset: f32,
51 pub cluster: u32,
53 pub is_whitespace: bool,
58 pub unsafe_to_break: bool,
62}
63
64impl Default for ShapedGlyph {
65 fn default() -> Self {
67 Self {
68 gid: 0,
69 x_advance: 0.0,
70 y_advance: 0.0,
71 x_offset: 0.0,
72 y_offset: 0.0,
73 cluster: 0,
74 is_whitespace: false,
75 unsafe_to_break: false,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct FontVerticalMetrics {
93 pub units_per_em: u16,
95 pub ascender: i16,
97 pub descender: i16,
99 pub line_gap: i16,
101}
102
103impl FontVerticalMetrics {
104 pub fn ascent_px(&self, font_size_px: f32) -> f32 {
106 if self.units_per_em == 0 {
107 return font_size_px * 0.8;
108 }
109 self.ascender as f32 * font_size_px / self.units_per_em as f32
110 }
111
112 pub fn descent_px(&self, font_size_px: f32) -> f32 {
114 if self.units_per_em == 0 {
115 return font_size_px * 0.2;
116 }
117 (-(self.descender as f32)) * font_size_px / self.units_per_em as f32
118 }
119
120 pub fn line_gap_px(&self, font_size_px: f32) -> f32 {
122 if self.units_per_em == 0 {
123 return font_size_px * 0.4;
124 }
125 self.line_gap as f32 * font_size_px / self.units_per_em as f32
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub struct GlyphMetrics {
139 pub bearing_x: f32,
141 pub bearing_y: f32,
143 pub advance_x: f32,
145 pub advance_y: f32,
147 pub width: f32,
149 pub height: f32,
151}
152
153impl Default for GlyphMetrics {
154 fn default() -> Self {
155 Self {
156 bearing_x: 0.0,
157 bearing_y: 0.0,
158 advance_x: 0.0,
159 advance_y: 0.0,
160 width: 0.0,
161 height: 0.0,
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
173pub struct GlyphCluster {
174 pub glyphs: Vec<ShapedGlyph>,
176 pub source_start: u32,
178 pub source_end: u32,
180}
181
182impl GlyphCluster {
183 pub fn advance(&self) -> f32 {
185 self.glyphs.iter().map(|g| g.x_advance).sum()
186 }
187
188 pub fn is_empty(&self) -> bool {
190 self.glyphs.is_empty()
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct ShapedRun {
197 pub glyphs: SmallVec<[ShapedGlyph; 8]>,
202 pub font_data: Arc<[u8]>,
204}
205
206#[derive(Debug, Clone)]
208pub struct PositionedGlyph {
209 pub gid: u16,
211 pub font_data: Arc<[u8]>,
213 pub pos: (f32, f32),
215 pub font_size: f32,
221 pub advance_x: f32,
226 pub cluster: u32,
232}
233
234#[derive(Debug, Clone)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237pub struct Bitmap {
238 pub width: u32,
240 pub height: u32,
242 pub pixels: Vec<u8>,
244}
245
246impl Bitmap {
247 pub fn is_empty(&self) -> bool {
250 self.width == 0 || self.height == 0 || self.pixels.is_empty()
251 }
252
253 pub fn invert_coverage(&self) -> Self {
259 Bitmap {
260 width: self.width,
261 height: self.height,
262 pixels: self.pixels.iter().map(|&v| 255 - v).collect(),
263 }
264 }
265
266 pub fn threshold(&self, threshold: u8) -> Self {
272 Bitmap {
273 width: self.width,
274 height: self.height,
275 pixels: self
276 .pixels
277 .iter()
278 .map(|&v| if v >= threshold { 255 } else { 0 })
279 .collect(),
280 }
281 }
282
283 pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> Self {
286 let mut pixels = vec![0u8; (width * height) as usize];
287 for row in 0..height {
288 for col in 0..width {
289 let src_x = x + col;
290 let src_y = y + row;
291 if src_x < self.width && src_y < self.height {
292 let src_idx = (src_y * self.width + src_x) as usize;
293 let dst_idx = (row * width + col) as usize;
294 pixels[dst_idx] = self.pixels[src_idx];
295 }
296 }
297 }
298 Bitmap {
299 width,
300 height,
301 pixels,
302 }
303 }
304
305 pub fn tight_bounds(&self) -> Option<(u32, u32, u32, u32)> {
311 let mut x_min = self.width;
312 let mut y_min = self.height;
313 let mut x_max = 0u32;
314 let mut y_max = 0u32;
315
316 for row in 0..self.height {
317 for col in 0..self.width {
318 if self.pixels[(row * self.width + col) as usize] > 0 {
319 x_min = x_min.min(col);
320 y_min = y_min.min(row);
321 x_max = x_max.max(col);
322 y_max = y_max.max(row);
323 }
324 }
325 }
326
327 if x_min > x_max {
328 None
329 } else {
330 Some((x_min, y_min, x_max, y_max))
331 }
332 }
333}
334
335#[derive(Debug, Clone)]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341pub struct ColorBitmap {
342 pub width: u32,
344 pub height: u32,
346 pub rgba: Vec<u8>,
348}
349
350impl ColorBitmap {
351 pub fn is_empty(&self) -> bool {
353 self.width == 0 || self.height == 0 || self.rgba.is_empty()
354 }
355}
356
357#[derive(Debug, Clone)]
366#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
367pub struct LcdBitmap {
368 pub width: u32,
370 pub height: u32,
372 pub rgb: Vec<u8>,
374}
375
376impl LcdBitmap {
377 pub fn new(width: u32, height: u32, rgb: Vec<u8>) -> Self {
384 debug_assert_eq!(
385 rgb.len(),
386 (width as usize) * (height as usize) * 3,
387 "LcdBitmap: rgb buffer length must equal width * height * 3"
388 );
389 Self { width, height, rgb }
390 }
391
392 pub fn is_empty(&self) -> bool {
394 self.width == 0 || self.height == 0 || self.rgb.is_empty()
395 }
396}
397
398#[derive(Debug, Clone)]
404#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
405pub enum RenderOutput {
406 Greyscale(Bitmap),
408 Color(ColorBitmap),
410 Sdf {
412 width: u32,
414 height: u32,
416 data: Vec<u8>,
418 },
419 Lcd(LcdBitmap),
424 Msdf {
430 width: u32,
432 height: u32,
434 data: Vec<u8>,
436 },
437}
438
439#[derive(Debug, Clone)]
441#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
442pub struct LayoutConstraints {
443 pub max_width: f32,
445 pub font_size: f32,
447}
448
449impl Default for LayoutConstraints {
450 fn default() -> Self {
451 Self {
452 max_width: 800.0,
453 font_size: 16.0,
454 }
455 }
456}
457
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
464#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
465pub enum FlowDirection {
466 #[default]
468 Horizontal,
469 Vertical,
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
477#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
478pub enum TextAlignment {
479 #[default]
481 Left,
482 Right,
484 Center,
486 Justify,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
498#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
499pub enum WritingMode {
500 #[default]
502 HorizontalTb,
503 VerticalRl,
505 VerticalLr,
507}
508
509impl WritingMode {
510 pub fn flow_direction(self) -> FlowDirection {
512 match self {
513 WritingMode::HorizontalTb => FlowDirection::Horizontal,
514 WritingMode::VerticalRl | WritingMode::VerticalLr => FlowDirection::Vertical,
515 }
516 }
517
518 pub fn is_vertical(self) -> bool {
520 !matches!(self, WritingMode::HorizontalTb)
521 }
522}
523
524#[derive(Debug, Clone, Copy, PartialEq)]
530#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
531pub struct LineSpacing {
532 pub leading: f32,
534 pub line_height_multiplier: f32,
536}
537
538impl Default for LineSpacing {
539 fn default() -> Self {
540 Self {
541 leading: 0.0,
542 line_height_multiplier: 1.0,
543 }
544 }
545}
546
547impl LineSpacing {
548 pub fn resolve(&self, natural_line_height: f32) -> f32 {
550 natural_line_height * self.line_height_multiplier + self.leading
551 }
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557pub struct Rgba8 {
558 pub r: u8,
560 pub g: u8,
562 pub b: u8,
564 pub a: u8,
566}
567
568impl Rgba8 {
569 pub const BLACK: Rgba8 = Rgba8 {
571 r: 0,
572 g: 0,
573 b: 0,
574 a: 255,
575 };
576 pub const TRANSPARENT: Rgba8 = Rgba8 {
578 r: 0,
579 g: 0,
580 b: 0,
581 a: 0,
582 };
583
584 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
586 Self { r, g, b, a }
587 }
588}
589
590impl Default for Rgba8 {
591 fn default() -> Self {
592 Rgba8::BLACK
593 }
594}
595
596#[derive(Debug, Clone, Copy, PartialEq)]
600#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
601pub struct DecorationLine {
602 pub position: f32,
606 pub thickness: f32,
608 pub color: Rgba8,
610}
611
612#[derive(Debug, Clone, Copy, PartialEq)]
618#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
619pub enum TextDecoration {
620 Underline {
622 color: Rgba8,
624 thickness: f32,
626 offset: f32,
628 },
629 Overline {
631 color: Rgba8,
633 thickness: f32,
635 offset: f32,
638 },
639 Strikethrough {
642 color: Rgba8,
644 thickness: f32,
646 },
647}
648
649#[derive(Debug, Clone, Copy, PartialEq)]
657#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
658pub struct DecorationRect {
659 pub x: f32,
661 pub y: f32,
663 pub width: f32,
665 pub height: f32,
667 pub color: Rgba8,
669}
670
671#[derive(Debug, Clone, Copy, PartialEq, Default)]
676#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
677pub struct Decoration {
678 pub underline: Option<DecorationLine>,
680 pub overline: Option<DecorationLine>,
682 pub strikethrough: Option<DecorationLine>,
684}
685
686impl Decoration {
687 pub fn any(&self) -> bool {
689 self.underline.is_some() || self.overline.is_some() || self.strikethrough.is_some()
690 }
691}
692
693#[derive(Debug, Clone)]
695#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
696pub struct TextStyle {
697 pub font_size: f32,
699 pub max_width: f32,
701 pub flow_direction: FlowDirection,
703 pub alignment: TextAlignment,
705 pub line_spacing: LineSpacing,
707}
708
709impl Default for TextStyle {
710 fn default() -> Self {
711 Self {
712 font_size: 16.0,
713 max_width: 800.0,
714 flow_direction: FlowDirection::Horizontal,
715 alignment: TextAlignment::Left,
716 line_spacing: LineSpacing::default(),
717 }
718 }
719}
720
721impl TextStyle {
722 pub fn with_alignment(mut self, alignment: TextAlignment) -> Self {
724 self.alignment = alignment;
725 self
726 }
727
728 pub fn with_font_size(mut self, font_size: f32) -> Self {
730 self.font_size = font_size;
731 self
732 }
733
734 pub fn with_max_width(mut self, max_width: f32) -> Self {
736 self.max_width = max_width;
737 self
738 }
739
740 pub fn with_flow_direction(mut self, flow_direction: FlowDirection) -> Self {
742 self.flow_direction = flow_direction;
743 self
744 }
745}
746
747#[derive(Debug, Clone)]
752#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
753pub struct ParagraphStyle {
754 pub alignment: TextAlignment,
756 pub indent: f32,
758 pub spacing_before: f32,
760 pub spacing_after: f32,
762 pub direction: FlowDirection,
764 pub line_spacing: LineSpacing,
766}
767
768impl Default for ParagraphStyle {
769 fn default() -> Self {
770 Self {
771 alignment: TextAlignment::Left,
772 indent: 0.0,
773 spacing_before: 0.0,
774 spacing_after: 0.0,
775 direction: FlowDirection::Horizontal,
776 line_spacing: LineSpacing::default(),
777 }
778 }
779}
780
781#[derive(Debug, Clone)]
787pub struct TextRun {
788 pub text: String,
790 pub font_data: Arc<[u8]>,
792 pub style: TextStyle,
794 pub decoration: Decoration,
796}
797
798#[derive(Debug, Clone, PartialEq)]
801pub struct InlineObject {
802 pub id: u64,
804 pub width: f32,
806 pub height: f32,
808 pub baseline_offset: f32,
810 pub advance: f32,
812}
813
814#[derive(Debug, Clone, PartialEq)]
816pub struct PositionedInlineObject {
817 pub object: InlineObject,
819 pub x: f32,
821 pub y: f32,
823 pub line: usize,
825}
826
827#[derive(Debug, Clone, Copy, PartialEq, Default)]
829pub enum VerticalPosition {
830 #[default]
832 Normal,
833 Superscript {
835 size_ratio: f32,
837 baseline_rise: f32,
839 },
840 Subscript {
842 size_ratio: f32,
844 baseline_drop: f32,
846 },
847}
848
849impl VerticalPosition {
850 pub fn effective_size(&self, base_px: f32) -> f32 {
852 match self {
853 Self::Normal => base_px,
854 Self::Superscript { size_ratio, .. } => base_px * size_ratio,
855 Self::Subscript { size_ratio, .. } => base_px * size_ratio,
856 }
857 }
858
859 pub fn baseline_adjustment(&self, _base_px: f32) -> f32 {
861 match self {
862 Self::Normal => 0.0,
863 Self::Superscript { baseline_rise, .. } => *baseline_rise,
864 Self::Subscript { baseline_drop, .. } => -*baseline_drop,
865 }
866 }
867}
868
869#[derive(Debug)]
871pub enum OxiTextError {
872 Shaping(String),
874 Layout(String),
876 Raster(String),
878 FontNotFound,
880 InvalidFont,
882 Other(String),
884}
885
886impl core::fmt::Display for OxiTextError {
887 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
888 match self {
889 OxiTextError::Shaping(s) => write!(f, "shaping error: {s}"),
890 OxiTextError::Layout(s) => write!(f, "layout error: {s}"),
891 OxiTextError::Raster(s) => write!(f, "raster error: {s}"),
892 OxiTextError::FontNotFound => write!(f, "font not found"),
893 OxiTextError::InvalidFont => write!(f, "invalid font"),
894 OxiTextError::Other(s) => write!(f, "text error: {s}"),
895 }
896 }
897}
898
899impl core::error::Error for OxiTextError {}
900
901impl RenderOutput {
902 pub fn into_bitmap(self) -> Option<Bitmap> {
905 match self {
906 RenderOutput::Greyscale(b) => Some(b),
907 _ => None,
908 }
909 }
910}
911
912impl From<RenderOutput> for Option<Bitmap> {
913 fn from(output: RenderOutput) -> Self {
916 output.into_bitmap()
917 }
918}
919
920#[cfg(all(test, feature = "std"))]
921mod tests {
922 use super::*;
923 use std::sync::Arc;
924
925 #[test]
926 fn layout_constraints_default_values() {
927 let c = LayoutConstraints::default();
928 assert_eq!(c.max_width, 800.0);
929 assert_eq!(c.font_size, 16.0);
930 }
931
932 #[test]
933 fn text_style_default_values() {
934 let s = TextStyle::default();
935 assert_eq!(s.font_size, 16.0);
936 assert_eq!(s.max_width, 800.0);
937 assert_eq!(s.flow_direction, FlowDirection::Horizontal);
938 assert_eq!(s.alignment, TextAlignment::Left);
939 assert_eq!(s.line_spacing.line_height_multiplier, 1.0);
940 }
941
942 #[test]
943 fn text_style_builders() {
944 let s = TextStyle::default()
945 .with_alignment(TextAlignment::Center)
946 .with_font_size(24.0)
947 .with_max_width(400.0);
948 assert_eq!(s.alignment, TextAlignment::Center);
949 assert_eq!(s.font_size, 24.0);
950 assert_eq!(s.max_width, 400.0);
951 }
952
953 #[test]
954 fn shaped_glyph_default_is_notdef() {
955 let g = ShapedGlyph::default();
956 assert_eq!(g.gid, 0);
957 assert_eq!(g.x_advance, 0.0);
958 assert!(!g.is_whitespace);
959 assert!(!g.unsafe_to_break);
960 }
961
962 #[test]
963 fn glyph_metrics_default_is_zero() {
964 let m = GlyphMetrics::default();
965 assert_eq!(m.advance_x, 0.0);
966 assert_eq!(m.width, 0.0);
967 }
968
969 #[test]
970 fn writing_mode_flow_direction_mapping() {
971 assert_eq!(
972 WritingMode::HorizontalTb.flow_direction(),
973 FlowDirection::Horizontal
974 );
975 assert_eq!(
976 WritingMode::VerticalRl.flow_direction(),
977 FlowDirection::Vertical
978 );
979 assert_eq!(
980 WritingMode::VerticalLr.flow_direction(),
981 FlowDirection::Vertical
982 );
983 assert!(!WritingMode::HorizontalTb.is_vertical());
984 assert!(WritingMode::VerticalRl.is_vertical());
985 }
986
987 #[test]
988 fn line_spacing_resolve() {
989 let ls = LineSpacing {
990 leading: 2.0,
991 line_height_multiplier: 1.5,
992 };
993 assert!((ls.resolve(20.0) - 32.0).abs() < f32::EPSILON);
995 let def = LineSpacing::default();
996 assert!((def.resolve(20.0) - 20.0).abs() < f32::EPSILON);
997 }
998
999 #[test]
1000 fn decoration_any_flag() {
1001 let none = Decoration::default();
1002 assert!(!none.any());
1003 let under = Decoration {
1004 underline: Some(DecorationLine {
1005 position: -2.0,
1006 thickness: 1.0,
1007 color: Rgba8::BLACK,
1008 }),
1009 ..Default::default()
1010 };
1011 assert!(under.any());
1012 }
1013
1014 #[test]
1015 fn glyph_cluster_advance_and_empty() {
1016 let empty = GlyphCluster {
1017 glyphs: vec![],
1018 source_start: 0,
1019 source_end: 0,
1020 };
1021 assert!(empty.is_empty());
1022 assert_eq!(empty.advance(), 0.0);
1023
1024 let cluster = GlyphCluster {
1025 glyphs: vec![
1026 ShapedGlyph {
1027 x_advance: 10.0,
1028 ..Default::default()
1029 },
1030 ShapedGlyph {
1031 x_advance: 5.0,
1032 ..Default::default()
1033 },
1034 ],
1035 source_start: 0,
1036 source_end: 3,
1037 };
1038 assert!(!cluster.is_empty());
1039 assert!((cluster.advance() - 15.0).abs() < f32::EPSILON);
1040 }
1041
1042 #[test]
1043 fn bitmap_and_color_bitmap_empty() {
1044 let bm = Bitmap {
1045 width: 0,
1046 height: 0,
1047 pixels: vec![],
1048 };
1049 assert!(bm.is_empty());
1050 let cbm = ColorBitmap {
1051 width: 2,
1052 height: 2,
1053 rgba: vec![0; 16],
1054 };
1055 assert!(!cbm.is_empty());
1056 }
1057
1058 #[test]
1059 fn render_output_variants_construct() {
1060 let g = RenderOutput::Greyscale(Bitmap {
1061 width: 1,
1062 height: 1,
1063 pixels: vec![255],
1064 });
1065 let c = RenderOutput::Color(ColorBitmap {
1066 width: 1,
1067 height: 1,
1068 rgba: vec![0, 0, 0, 255],
1069 });
1070 let s = RenderOutput::Sdf {
1071 width: 1,
1072 height: 1,
1073 data: vec![128],
1074 };
1075 let lcd = RenderOutput::Lcd(LcdBitmap::new(1, 1, vec![255, 0, 0]));
1076 let msdf = RenderOutput::Msdf {
1077 width: 1,
1078 height: 1,
1079 data: vec![100, 128, 200],
1080 };
1081 assert!(matches!(g, RenderOutput::Greyscale(_)));
1083 assert!(matches!(c, RenderOutput::Color(_)));
1084 assert!(matches!(s, RenderOutput::Sdf { .. }));
1085 assert!(matches!(lcd, RenderOutput::Lcd(_)));
1086 assert!(matches!(msdf, RenderOutput::Msdf { .. }));
1087 }
1088
1089 #[test]
1090 fn lcd_bitmap_new_constructor() {
1091 let bm = LcdBitmap::new(4, 2, vec![0u8; 4 * 2 * 3]);
1092 assert_eq!(bm.width, 4);
1093 assert_eq!(bm.height, 2);
1094 assert_eq!(bm.rgb.len(), 24);
1095 assert!(!bm.is_empty());
1096 }
1097
1098 #[test]
1099 fn lcd_bitmap_is_empty() {
1100 let empty_w = LcdBitmap {
1101 width: 0,
1102 height: 1,
1103 rgb: vec![],
1104 };
1105 assert!(empty_w.is_empty());
1106 let empty_h = LcdBitmap {
1107 width: 1,
1108 height: 0,
1109 rgb: vec![],
1110 };
1111 assert!(empty_h.is_empty());
1112 let empty_buf = LcdBitmap {
1113 width: 1,
1114 height: 1,
1115 rgb: vec![],
1116 };
1117 assert!(empty_buf.is_empty());
1118 }
1119
1120 #[test]
1121 fn msdf_variant_fields() {
1122 let msdf = RenderOutput::Msdf {
1123 width: 8,
1124 height: 8,
1125 data: vec![0u8; 8 * 8 * 3],
1126 };
1127 if let RenderOutput::Msdf {
1128 width,
1129 height,
1130 data,
1131 } = &msdf
1132 {
1133 assert_eq!(*width, 8);
1134 assert_eq!(*height, 8);
1135 assert_eq!(data.len(), 192);
1136 } else {
1137 panic!("expected Msdf variant");
1138 }
1139 }
1140
1141 #[test]
1142 fn positioned_glyph_carries_font_size() {
1143 let pg = PositionedGlyph {
1144 gid: 5,
1145 font_data: Arc::from(&[][..]),
1146 pos: (1.0, 2.0),
1147 font_size: 18.0,
1148 advance_x: 12.0,
1149 cluster: 0,
1150 };
1151 assert_eq!(pg.font_size, 18.0);
1152 }
1153
1154 #[test]
1155 fn text_run_construction() {
1156 let run = TextRun {
1157 text: "hi".to_string(),
1158 font_data: Arc::from(&[][..]),
1159 style: TextStyle::default(),
1160 decoration: Decoration::default(),
1161 };
1162 assert_eq!(run.text, "hi");
1163 assert!(!run.decoration.any());
1164 }
1165
1166 #[test]
1167 fn flow_direction_is_hashable() {
1168 use std::collections::HashSet;
1169 let mut set = HashSet::new();
1170 set.insert(FlowDirection::Horizontal);
1171 set.insert(FlowDirection::Vertical);
1172 set.insert(FlowDirection::Horizontal);
1173 assert_eq!(set.len(), 2);
1174 }
1175
1176 #[test]
1177 fn text_alignment_is_hashable() {
1178 use std::collections::HashMap;
1179 let mut map = HashMap::new();
1180 map.insert(TextAlignment::Left, 1);
1181 map.insert(TextAlignment::Center, 2);
1182 assert_eq!(map.get(&TextAlignment::Left), Some(&1));
1183 }
1184
1185 #[test]
1186 fn oxitext_error_display_all_variants() {
1187 assert_eq!(
1188 OxiTextError::Shaping("x".into()).to_string(),
1189 "shaping error: x"
1190 );
1191 assert_eq!(
1192 OxiTextError::Layout("x".into()).to_string(),
1193 "layout error: x"
1194 );
1195 assert_eq!(
1196 OxiTextError::Raster("x".into()).to_string(),
1197 "raster error: x"
1198 );
1199 assert_eq!(OxiTextError::FontNotFound.to_string(), "font not found");
1200 assert_eq!(OxiTextError::InvalidFont.to_string(), "invalid font");
1201 assert_eq!(OxiTextError::Other("x".into()).to_string(), "text error: x");
1202 }
1203
1204 #[test]
1207 fn test_flow_direction_equality() {
1208 assert_eq!(FlowDirection::Horizontal, FlowDirection::Horizontal);
1209 assert_ne!(FlowDirection::Horizontal, FlowDirection::Vertical);
1210 }
1211
1212 #[test]
1213 fn test_flow_direction_clone() {
1214 let a = FlowDirection::Vertical;
1215 #[allow(clippy::clone_on_copy)]
1216 let b = Clone::clone(&a);
1217 assert_eq!(a, b);
1218 }
1219
1220 #[test]
1221 fn test_flow_direction_debug() {
1222 let s = format!("{:?}", FlowDirection::Horizontal);
1223 assert!(s.contains("Horizontal"));
1224 }
1225
1226 #[test]
1227 fn test_text_alignment_ordering() {
1228 assert_eq!(TextAlignment::Left, TextAlignment::Left);
1230 assert_ne!(TextAlignment::Left, TextAlignment::Right);
1231 }
1232
1233 #[test]
1236 fn test_shaped_glyph_negative_offsets() {
1237 let g = ShapedGlyph {
1239 gid: 0x301, x_advance: 0.0, y_advance: 0.0,
1242 x_offset: -2.5, y_offset: -8.0, cluster: 0,
1245 is_whitespace: false,
1246 unsafe_to_break: true, };
1248 assert!(g.x_offset < 0.0);
1249 assert!(g.y_offset < 0.0);
1250 assert!(g.unsafe_to_break);
1251 assert_eq!(g.x_advance, 0.0);
1252 }
1253
1254 #[test]
1255 fn test_shaped_glyph_default_is_notdef() {
1256 let g = ShapedGlyph::default();
1257 assert_eq!(g.gid, 0);
1258 assert_eq!(g.x_advance, 0.0);
1259 assert!(!g.unsafe_to_break);
1260 }
1261
1262 #[test]
1265 fn test_error_display() {
1266 let e = OxiTextError::FontNotFound;
1267 let s = format!("{e}");
1268 assert!(!s.is_empty());
1269 }
1270
1271 #[test]
1272 fn test_error_invalid_font() {
1273 let e = OxiTextError::InvalidFont;
1274 assert_ne!(format!("{e}"), format!("{}", OxiTextError::FontNotFound));
1275 }
1276
1277 #[test]
1278 fn types_are_send_sync() {
1279 fn assert_send_sync<T: Send + Sync>() {}
1280 assert_send_sync::<ShapedGlyph>();
1281 assert_send_sync::<ShapedRun>();
1282 assert_send_sync::<PositionedGlyph>();
1283 assert_send_sync::<Bitmap>();
1284 assert_send_sync::<ColorBitmap>();
1285 assert_send_sync::<LcdBitmap>();
1286 assert_send_sync::<RenderOutput>();
1287 assert_send_sync::<TextStyle>();
1288 assert_send_sync::<ParagraphStyle>();
1289 assert_send_sync::<TextRun>();
1290 assert_send_sync::<GlyphCluster>();
1291 assert_send_sync::<GlyphMetrics>();
1292 }
1293
1294 #[test]
1295 fn render_output_into_bitmap_greyscale() {
1296 let bm = Bitmap {
1297 width: 4,
1298 height: 4,
1299 pixels: vec![255u8; 16],
1300 };
1301 let out = RenderOutput::Greyscale(bm.clone());
1302 let extracted: Option<Bitmap> = out.into();
1303 assert!(extracted.is_some());
1304 let extracted = extracted.expect("greyscale should yield Some(Bitmap)");
1305 assert_eq!(extracted.width, 4);
1306 assert_eq!(extracted.pixels.len(), 16);
1307 }
1308
1309 #[test]
1310 fn render_output_into_bitmap_non_greyscale_is_none() {
1311 let out = RenderOutput::Sdf {
1312 width: 4,
1313 height: 4,
1314 data: vec![128u8; 16],
1315 };
1316 let extracted: Option<Bitmap> = out.into();
1317 assert!(extracted.is_none());
1318
1319 let out2 = RenderOutput::Msdf {
1320 width: 4,
1321 height: 4,
1322 data: vec![100u8; 48],
1323 };
1324 let extracted2: Option<Bitmap> = out2.into();
1325 assert!(extracted2.is_none());
1326 }
1327
1328 #[cfg(feature = "serde")]
1329 #[test]
1330 fn serde_roundtrip_bitmap() {
1331 let bm = Bitmap {
1332 width: 2,
1333 height: 2,
1334 pixels: vec![0, 128, 200, 255],
1335 };
1336 let json = serde_json::to_string(&bm).expect("serialize Bitmap");
1337 let back: Bitmap = serde_json::from_str(&json).expect("deserialize Bitmap");
1338 assert_eq!(back.width, bm.width);
1339 assert_eq!(back.pixels, bm.pixels);
1340 }
1341
1342 #[test]
1343 fn test_decoration_rect_fields() {
1344 let r = DecorationRect {
1345 x: 1.0,
1346 y: 2.0,
1347 width: 10.0,
1348 height: 1.5,
1349 color: Rgba8 {
1350 r: 0,
1351 g: 0,
1352 b: 0,
1353 a: 255,
1354 },
1355 };
1356 assert_eq!(r.width, 10.0);
1357 assert_eq!(r.height, 1.5);
1358 assert_eq!(r.color.a, 255);
1359 }
1360
1361 #[test]
1362 fn test_text_decoration_variants() {
1363 let under = TextDecoration::Underline {
1364 color: Rgba8::BLACK,
1365 thickness: 1.0,
1366 offset: 2.0,
1367 };
1368 let over = TextDecoration::Overline {
1369 color: Rgba8::BLACK,
1370 thickness: 1.0,
1371 offset: 0.0,
1372 };
1373 let strike = TextDecoration::Strikethrough {
1374 color: Rgba8::BLACK,
1375 thickness: 1.5,
1376 };
1377 assert_ne!(under, over);
1378 assert_ne!(under, strike);
1379 let _copy = under;
1381 let _copy2 = over;
1382 }
1383
1384 #[cfg(feature = "serde")]
1385 #[test]
1386 fn serde_roundtrip_text_style() {
1387 let style = TextStyle {
1388 font_size: 24.0,
1389 max_width: 600.0,
1390 flow_direction: FlowDirection::Vertical,
1391 alignment: TextAlignment::Center,
1392 line_spacing: LineSpacing {
1393 leading: 2.0,
1394 line_height_multiplier: 1.5,
1395 },
1396 };
1397 let json = serde_json::to_string(&style).expect("serialize TextStyle");
1398 let back: TextStyle = serde_json::from_str(&json).expect("deserialize TextStyle");
1399 assert_eq!(back.font_size, 24.0);
1400 assert_eq!(back.alignment, TextAlignment::Center);
1401 assert_eq!(back.flow_direction, FlowDirection::Vertical);
1402 }
1403
1404 #[test]
1407 fn test_bitmap_invert_coverage() {
1408 let b = Bitmap {
1409 width: 2,
1410 height: 1,
1411 pixels: vec![0u8, 255],
1412 };
1413 let inv = b.invert_coverage();
1414 assert_eq!(inv.pixels[0], 255);
1415 assert_eq!(inv.pixels[1], 0);
1416 }
1417
1418 #[test]
1419 fn test_bitmap_threshold() {
1420 let b = Bitmap {
1421 width: 3,
1422 height: 1,
1423 pixels: vec![64u8, 128, 200],
1424 };
1425 let t = b.threshold(128);
1426 assert_eq!(t.pixels[0], 0);
1427 assert_eq!(t.pixels[1], 255);
1428 assert_eq!(t.pixels[2], 255);
1429 }
1430
1431 #[test]
1432 fn test_bitmap_tight_bounds_all_zero_returns_none() {
1433 let b = Bitmap {
1434 width: 4,
1435 height: 4,
1436 pixels: vec![0u8; 16],
1437 };
1438 assert!(b.tight_bounds().is_none());
1439 }
1440
1441 #[test]
1442 fn test_bitmap_tight_bounds_single_pixel() {
1443 let mut pixels = vec![0u8; 16];
1444 pixels[4 * 2 + 1] = 255; let b = Bitmap {
1446 width: 4,
1447 height: 4,
1448 pixels,
1449 };
1450 let bounds = b.tight_bounds().expect("should find pixel");
1451 assert_eq!(bounds, (1, 2, 1, 2));
1452 }
1453
1454 #[test]
1455 fn test_bitmap_crop() {
1456 let pixels = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
1457 let b = Bitmap {
1458 width: 4,
1459 height: 4,
1460 pixels,
1461 };
1462 let cropped = b.crop(1, 1, 2, 2);
1463 assert_eq!(cropped.width, 2);
1464 assert_eq!(cropped.height, 2);
1465 assert_eq!(cropped.pixels, vec![6u8, 7, 10, 11]);
1466 }
1467
1468 #[test]
1469 fn test_bitmap_invert_is_involution() {
1470 let b = Bitmap {
1471 width: 3,
1472 height: 1,
1473 pixels: vec![10u8, 128, 200],
1474 };
1475 let double_inv = b.invert_coverage().invert_coverage();
1476 assert_eq!(double_inv.pixels, b.pixels);
1477 }
1478
1479 #[test]
1480 fn test_bitmap_crop_out_of_bounds_fills_zero() {
1481 let b = Bitmap {
1482 width: 2,
1483 height: 2,
1484 pixels: vec![1u8, 2, 3, 4],
1485 };
1486 let cropped = b.crop(5, 5, 3, 3);
1488 assert_eq!(cropped.pixels, vec![0u8; 9]);
1489 }
1490
1491 #[test]
1492 fn test_std_feature_enabled_by_default() {
1493 #[cfg(feature = "std")]
1496 {
1497 let err: &dyn core::error::Error = &OxiTextError::InvalidFont;
1499 let _ = err.to_string();
1500 }
1501 }
1502
1503 #[test]
1504 fn test_vertical_position_effective_size() {
1505 let vp = VerticalPosition::Superscript {
1506 size_ratio: 0.6,
1507 baseline_rise: 4.0,
1508 };
1509 assert!((vp.effective_size(16.0) - 9.6).abs() < 0.001);
1510 }
1511
1512 #[test]
1513 fn test_vertical_position_baseline_adjustment() {
1514 let sub = VerticalPosition::Subscript {
1515 size_ratio: 0.6,
1516 baseline_drop: 3.0,
1517 };
1518 assert_eq!(sub.baseline_adjustment(16.0), -3.0);
1519 let norm = VerticalPosition::Normal;
1520 assert_eq!(norm.baseline_adjustment(16.0), 0.0);
1521 }
1522}