#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{string::String, sync::Arc, vec, vec::Vec};
#[cfg(feature = "std")]
use std::sync::Arc;
use smallvec::SmallVec;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShapedGlyph {
pub gid: u16,
pub x_advance: f32,
pub y_advance: f32,
pub x_offset: f32,
pub y_offset: f32,
pub cluster: u32,
pub is_whitespace: bool,
pub unsafe_to_break: bool,
}
impl Default for ShapedGlyph {
fn default() -> Self {
Self {
gid: 0,
x_advance: 0.0,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
cluster: 0,
is_whitespace: false,
unsafe_to_break: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FontVerticalMetrics {
pub units_per_em: u16,
pub ascender: i16,
pub descender: i16,
pub line_gap: i16,
}
impl FontVerticalMetrics {
pub fn ascent_px(&self, font_size_px: f32) -> f32 {
if self.units_per_em == 0 {
return font_size_px * 0.8;
}
self.ascender as f32 * font_size_px / self.units_per_em as f32
}
pub fn descent_px(&self, font_size_px: f32) -> f32 {
if self.units_per_em == 0 {
return font_size_px * 0.2;
}
(-(self.descender as f32)) * font_size_px / self.units_per_em as f32
}
pub fn line_gap_px(&self, font_size_px: f32) -> f32 {
if self.units_per_em == 0 {
return font_size_px * 0.4;
}
self.line_gap as f32 * font_size_px / self.units_per_em as f32
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GlyphMetrics {
pub bearing_x: f32,
pub bearing_y: f32,
pub advance_x: f32,
pub advance_y: f32,
pub width: f32,
pub height: f32,
}
impl Default for GlyphMetrics {
fn default() -> Self {
Self {
bearing_x: 0.0,
bearing_y: 0.0,
advance_x: 0.0,
advance_y: 0.0,
width: 0.0,
height: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct GlyphCluster {
pub glyphs: Vec<ShapedGlyph>,
pub source_start: u32,
pub source_end: u32,
}
impl GlyphCluster {
pub fn advance(&self) -> f32 {
self.glyphs.iter().map(|g| g.x_advance).sum()
}
pub fn is_empty(&self) -> bool {
self.glyphs.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ShapedRun {
pub glyphs: SmallVec<[ShapedGlyph; 8]>,
pub font_data: Arc<[u8]>,
}
#[derive(Debug, Clone)]
pub struct PositionedGlyph {
pub gid: u16,
pub font_data: Arc<[u8]>,
pub pos: (f32, f32),
pub font_size: f32,
pub advance_x: f32,
pub cluster: u32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bitmap {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
impl Bitmap {
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0 || self.pixels.is_empty()
}
pub fn invert_coverage(&self) -> Self {
Bitmap {
width: self.width,
height: self.height,
pixels: self.pixels.iter().map(|&v| 255 - v).collect(),
}
}
pub fn threshold(&self, threshold: u8) -> Self {
Bitmap {
width: self.width,
height: self.height,
pixels: self
.pixels
.iter()
.map(|&v| if v >= threshold { 255 } else { 0 })
.collect(),
}
}
pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> Self {
let mut pixels = vec![0u8; (width * height) as usize];
for row in 0..height {
for col in 0..width {
let src_x = x + col;
let src_y = y + row;
if src_x < self.width && src_y < self.height {
let src_idx = (src_y * self.width + src_x) as usize;
let dst_idx = (row * width + col) as usize;
pixels[dst_idx] = self.pixels[src_idx];
}
}
}
Bitmap {
width,
height,
pixels,
}
}
pub fn tight_bounds(&self) -> Option<(u32, u32, u32, u32)> {
let mut x_min = self.width;
let mut y_min = self.height;
let mut x_max = 0u32;
let mut y_max = 0u32;
for row in 0..self.height {
for col in 0..self.width {
if self.pixels[(row * self.width + col) as usize] > 0 {
x_min = x_min.min(col);
y_min = y_min.min(row);
x_max = x_max.max(col);
y_max = y_max.max(row);
}
}
}
if x_min > x_max {
None
} else {
Some((x_min, y_min, x_max, y_max))
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ColorBitmap {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
impl ColorBitmap {
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0 || self.rgba.is_empty()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LcdBitmap {
pub width: u32,
pub height: u32,
pub rgb: Vec<u8>,
}
impl LcdBitmap {
pub fn new(width: u32, height: u32, rgb: Vec<u8>) -> Self {
debug_assert_eq!(
rgb.len(),
(width as usize) * (height as usize) * 3,
"LcdBitmap: rgb buffer length must equal width * height * 3"
);
Self { width, height, rgb }
}
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0 || self.rgb.is_empty()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderOutput {
Greyscale(Bitmap),
Color(ColorBitmap),
Sdf {
width: u32,
height: u32,
data: Vec<u8>,
},
Lcd(LcdBitmap),
Msdf {
width: u32,
height: u32,
data: Vec<u8>,
},
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LayoutConstraints {
pub max_width: f32,
pub font_size: f32,
}
impl Default for LayoutConstraints {
fn default() -> Self {
Self {
max_width: 800.0,
font_size: 16.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FlowDirection {
#[default]
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TextAlignment {
#[default]
Left,
Right,
Center,
Justify,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WritingMode {
#[default]
HorizontalTb,
VerticalRl,
VerticalLr,
}
impl WritingMode {
pub fn flow_direction(self) -> FlowDirection {
match self {
WritingMode::HorizontalTb => FlowDirection::Horizontal,
WritingMode::VerticalRl | WritingMode::VerticalLr => FlowDirection::Vertical,
}
}
pub fn is_vertical(self) -> bool {
!matches!(self, WritingMode::HorizontalTb)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LineSpacing {
pub leading: f32,
pub line_height_multiplier: f32,
}
impl Default for LineSpacing {
fn default() -> Self {
Self {
leading: 0.0,
line_height_multiplier: 1.0,
}
}
}
impl LineSpacing {
pub fn resolve(&self, natural_line_height: f32) -> f32 {
natural_line_height * self.line_height_multiplier + self.leading
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rgba8 {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Rgba8 {
pub const BLACK: Rgba8 = Rgba8 {
r: 0,
g: 0,
b: 0,
a: 255,
};
pub const TRANSPARENT: Rgba8 = Rgba8 {
r: 0,
g: 0,
b: 0,
a: 0,
};
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
}
impl Default for Rgba8 {
fn default() -> Self {
Rgba8::BLACK
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DecorationLine {
pub position: f32,
pub thickness: f32,
pub color: Rgba8,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TextDecoration {
Underline {
color: Rgba8,
thickness: f32,
offset: f32,
},
Overline {
color: Rgba8,
thickness: f32,
offset: f32,
},
Strikethrough {
color: Rgba8,
thickness: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DecorationRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub color: Rgba8,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Decoration {
pub underline: Option<DecorationLine>,
pub overline: Option<DecorationLine>,
pub strikethrough: Option<DecorationLine>,
}
impl Decoration {
pub fn any(&self) -> bool {
self.underline.is_some() || self.overline.is_some() || self.strikethrough.is_some()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TextStyle {
pub font_size: f32,
pub max_width: f32,
pub flow_direction: FlowDirection,
pub alignment: TextAlignment,
pub line_spacing: LineSpacing,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font_size: 16.0,
max_width: 800.0,
flow_direction: FlowDirection::Horizontal,
alignment: TextAlignment::Left,
line_spacing: LineSpacing::default(),
}
}
}
impl TextStyle {
pub fn with_alignment(mut self, alignment: TextAlignment) -> Self {
self.alignment = alignment;
self
}
pub fn with_font_size(mut self, font_size: f32) -> Self {
self.font_size = font_size;
self
}
pub fn with_max_width(mut self, max_width: f32) -> Self {
self.max_width = max_width;
self
}
pub fn with_flow_direction(mut self, flow_direction: FlowDirection) -> Self {
self.flow_direction = flow_direction;
self
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ParagraphStyle {
pub alignment: TextAlignment,
pub indent: f32,
pub spacing_before: f32,
pub spacing_after: f32,
pub direction: FlowDirection,
pub line_spacing: LineSpacing,
}
impl Default for ParagraphStyle {
fn default() -> Self {
Self {
alignment: TextAlignment::Left,
indent: 0.0,
spacing_before: 0.0,
spacing_after: 0.0,
direction: FlowDirection::Horizontal,
line_spacing: LineSpacing::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct TextRun {
pub text: String,
pub font_data: Arc<[u8]>,
pub style: TextStyle,
pub decoration: Decoration,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InlineObject {
pub id: u64,
pub width: f32,
pub height: f32,
pub baseline_offset: f32,
pub advance: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PositionedInlineObject {
pub object: InlineObject,
pub x: f32,
pub y: f32,
pub line: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum VerticalPosition {
#[default]
Normal,
Superscript {
size_ratio: f32,
baseline_rise: f32,
},
Subscript {
size_ratio: f32,
baseline_drop: f32,
},
}
impl VerticalPosition {
pub fn effective_size(&self, base_px: f32) -> f32 {
match self {
Self::Normal => base_px,
Self::Superscript { size_ratio, .. } => base_px * size_ratio,
Self::Subscript { size_ratio, .. } => base_px * size_ratio,
}
}
pub fn baseline_adjustment(&self, _base_px: f32) -> f32 {
match self {
Self::Normal => 0.0,
Self::Superscript { baseline_rise, .. } => *baseline_rise,
Self::Subscript { baseline_drop, .. } => -*baseline_drop,
}
}
}
#[derive(Debug)]
pub enum OxiTextError {
Shaping(String),
Layout(String),
Raster(String),
FontNotFound,
InvalidFont,
Other(String),
}
impl core::fmt::Display for OxiTextError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
OxiTextError::Shaping(s) => write!(f, "shaping error: {s}"),
OxiTextError::Layout(s) => write!(f, "layout error: {s}"),
OxiTextError::Raster(s) => write!(f, "raster error: {s}"),
OxiTextError::FontNotFound => write!(f, "font not found"),
OxiTextError::InvalidFont => write!(f, "invalid font"),
OxiTextError::Other(s) => write!(f, "text error: {s}"),
}
}
}
impl core::error::Error for OxiTextError {}
impl RenderOutput {
pub fn into_bitmap(self) -> Option<Bitmap> {
match self {
RenderOutput::Greyscale(b) => Some(b),
_ => None,
}
}
}
impl From<RenderOutput> for Option<Bitmap> {
fn from(output: RenderOutput) -> Self {
output.into_bitmap()
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn layout_constraints_default_values() {
let c = LayoutConstraints::default();
assert_eq!(c.max_width, 800.0);
assert_eq!(c.font_size, 16.0);
}
#[test]
fn text_style_default_values() {
let s = TextStyle::default();
assert_eq!(s.font_size, 16.0);
assert_eq!(s.max_width, 800.0);
assert_eq!(s.flow_direction, FlowDirection::Horizontal);
assert_eq!(s.alignment, TextAlignment::Left);
assert_eq!(s.line_spacing.line_height_multiplier, 1.0);
}
#[test]
fn text_style_builders() {
let s = TextStyle::default()
.with_alignment(TextAlignment::Center)
.with_font_size(24.0)
.with_max_width(400.0);
assert_eq!(s.alignment, TextAlignment::Center);
assert_eq!(s.font_size, 24.0);
assert_eq!(s.max_width, 400.0);
}
#[test]
fn shaped_glyph_default_is_notdef() {
let g = ShapedGlyph::default();
assert_eq!(g.gid, 0);
assert_eq!(g.x_advance, 0.0);
assert!(!g.is_whitespace);
assert!(!g.unsafe_to_break);
}
#[test]
fn glyph_metrics_default_is_zero() {
let m = GlyphMetrics::default();
assert_eq!(m.advance_x, 0.0);
assert_eq!(m.width, 0.0);
}
#[test]
fn writing_mode_flow_direction_mapping() {
assert_eq!(
WritingMode::HorizontalTb.flow_direction(),
FlowDirection::Horizontal
);
assert_eq!(
WritingMode::VerticalRl.flow_direction(),
FlowDirection::Vertical
);
assert_eq!(
WritingMode::VerticalLr.flow_direction(),
FlowDirection::Vertical
);
assert!(!WritingMode::HorizontalTb.is_vertical());
assert!(WritingMode::VerticalRl.is_vertical());
}
#[test]
fn line_spacing_resolve() {
let ls = LineSpacing {
leading: 2.0,
line_height_multiplier: 1.5,
};
assert!((ls.resolve(20.0) - 32.0).abs() < f32::EPSILON);
let def = LineSpacing::default();
assert!((def.resolve(20.0) - 20.0).abs() < f32::EPSILON);
}
#[test]
fn decoration_any_flag() {
let none = Decoration::default();
assert!(!none.any());
let under = Decoration {
underline: Some(DecorationLine {
position: -2.0,
thickness: 1.0,
color: Rgba8::BLACK,
}),
..Default::default()
};
assert!(under.any());
}
#[test]
fn glyph_cluster_advance_and_empty() {
let empty = GlyphCluster {
glyphs: vec![],
source_start: 0,
source_end: 0,
};
assert!(empty.is_empty());
assert_eq!(empty.advance(), 0.0);
let cluster = GlyphCluster {
glyphs: vec![
ShapedGlyph {
x_advance: 10.0,
..Default::default()
},
ShapedGlyph {
x_advance: 5.0,
..Default::default()
},
],
source_start: 0,
source_end: 3,
};
assert!(!cluster.is_empty());
assert!((cluster.advance() - 15.0).abs() < f32::EPSILON);
}
#[test]
fn bitmap_and_color_bitmap_empty() {
let bm = Bitmap {
width: 0,
height: 0,
pixels: vec![],
};
assert!(bm.is_empty());
let cbm = ColorBitmap {
width: 2,
height: 2,
rgba: vec![0; 16],
};
assert!(!cbm.is_empty());
}
#[test]
fn render_output_variants_construct() {
let g = RenderOutput::Greyscale(Bitmap {
width: 1,
height: 1,
pixels: vec![255],
});
let c = RenderOutput::Color(ColorBitmap {
width: 1,
height: 1,
rgba: vec![0, 0, 0, 255],
});
let s = RenderOutput::Sdf {
width: 1,
height: 1,
data: vec![128],
};
let lcd = RenderOutput::Lcd(LcdBitmap::new(1, 1, vec![255, 0, 0]));
let msdf = RenderOutput::Msdf {
width: 1,
height: 1,
data: vec![100, 128, 200],
};
assert!(matches!(g, RenderOutput::Greyscale(_)));
assert!(matches!(c, RenderOutput::Color(_)));
assert!(matches!(s, RenderOutput::Sdf { .. }));
assert!(matches!(lcd, RenderOutput::Lcd(_)));
assert!(matches!(msdf, RenderOutput::Msdf { .. }));
}
#[test]
fn lcd_bitmap_new_constructor() {
let bm = LcdBitmap::new(4, 2, vec![0u8; 4 * 2 * 3]);
assert_eq!(bm.width, 4);
assert_eq!(bm.height, 2);
assert_eq!(bm.rgb.len(), 24);
assert!(!bm.is_empty());
}
#[test]
fn lcd_bitmap_is_empty() {
let empty_w = LcdBitmap {
width: 0,
height: 1,
rgb: vec![],
};
assert!(empty_w.is_empty());
let empty_h = LcdBitmap {
width: 1,
height: 0,
rgb: vec![],
};
assert!(empty_h.is_empty());
let empty_buf = LcdBitmap {
width: 1,
height: 1,
rgb: vec![],
};
assert!(empty_buf.is_empty());
}
#[test]
fn msdf_variant_fields() {
let msdf = RenderOutput::Msdf {
width: 8,
height: 8,
data: vec![0u8; 8 * 8 * 3],
};
if let RenderOutput::Msdf {
width,
height,
data,
} = &msdf
{
assert_eq!(*width, 8);
assert_eq!(*height, 8);
assert_eq!(data.len(), 192);
} else {
panic!("expected Msdf variant");
}
}
#[test]
fn positioned_glyph_carries_font_size() {
let pg = PositionedGlyph {
gid: 5,
font_data: Arc::from(&[][..]),
pos: (1.0, 2.0),
font_size: 18.0,
advance_x: 12.0,
cluster: 0,
};
assert_eq!(pg.font_size, 18.0);
}
#[test]
fn text_run_construction() {
let run = TextRun {
text: "hi".to_string(),
font_data: Arc::from(&[][..]),
style: TextStyle::default(),
decoration: Decoration::default(),
};
assert_eq!(run.text, "hi");
assert!(!run.decoration.any());
}
#[test]
fn flow_direction_is_hashable() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(FlowDirection::Horizontal);
set.insert(FlowDirection::Vertical);
set.insert(FlowDirection::Horizontal);
assert_eq!(set.len(), 2);
}
#[test]
fn text_alignment_is_hashable() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(TextAlignment::Left, 1);
map.insert(TextAlignment::Center, 2);
assert_eq!(map.get(&TextAlignment::Left), Some(&1));
}
#[test]
fn oxitext_error_display_all_variants() {
assert_eq!(
OxiTextError::Shaping("x".into()).to_string(),
"shaping error: x"
);
assert_eq!(
OxiTextError::Layout("x".into()).to_string(),
"layout error: x"
);
assert_eq!(
OxiTextError::Raster("x".into()).to_string(),
"raster error: x"
);
assert_eq!(OxiTextError::FontNotFound.to_string(), "font not found");
assert_eq!(OxiTextError::InvalidFont.to_string(), "invalid font");
assert_eq!(OxiTextError::Other("x".into()).to_string(), "text error: x");
}
#[test]
fn test_flow_direction_equality() {
assert_eq!(FlowDirection::Horizontal, FlowDirection::Horizontal);
assert_ne!(FlowDirection::Horizontal, FlowDirection::Vertical);
}
#[test]
fn test_flow_direction_clone() {
let a = FlowDirection::Vertical;
#[allow(clippy::clone_on_copy)]
let b = Clone::clone(&a);
assert_eq!(a, b);
}
#[test]
fn test_flow_direction_debug() {
let s = format!("{:?}", FlowDirection::Horizontal);
assert!(s.contains("Horizontal"));
}
#[test]
fn test_text_alignment_ordering() {
assert_eq!(TextAlignment::Left, TextAlignment::Left);
assert_ne!(TextAlignment::Left, TextAlignment::Right);
}
#[test]
fn test_shaped_glyph_negative_offsets() {
let g = ShapedGlyph {
gid: 0x301, x_advance: 0.0, y_advance: 0.0,
x_offset: -2.5, y_offset: -8.0, cluster: 0,
is_whitespace: false,
unsafe_to_break: true, };
assert!(g.x_offset < 0.0);
assert!(g.y_offset < 0.0);
assert!(g.unsafe_to_break);
assert_eq!(g.x_advance, 0.0);
}
#[test]
fn test_shaped_glyph_default_is_notdef() {
let g = ShapedGlyph::default();
assert_eq!(g.gid, 0);
assert_eq!(g.x_advance, 0.0);
assert!(!g.unsafe_to_break);
}
#[test]
fn test_error_display() {
let e = OxiTextError::FontNotFound;
let s = format!("{e}");
assert!(!s.is_empty());
}
#[test]
fn test_error_invalid_font() {
let e = OxiTextError::InvalidFont;
assert_ne!(format!("{e}"), format!("{}", OxiTextError::FontNotFound));
}
#[test]
fn types_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ShapedGlyph>();
assert_send_sync::<ShapedRun>();
assert_send_sync::<PositionedGlyph>();
assert_send_sync::<Bitmap>();
assert_send_sync::<ColorBitmap>();
assert_send_sync::<LcdBitmap>();
assert_send_sync::<RenderOutput>();
assert_send_sync::<TextStyle>();
assert_send_sync::<ParagraphStyle>();
assert_send_sync::<TextRun>();
assert_send_sync::<GlyphCluster>();
assert_send_sync::<GlyphMetrics>();
}
#[test]
fn render_output_into_bitmap_greyscale() {
let bm = Bitmap {
width: 4,
height: 4,
pixels: vec![255u8; 16],
};
let out = RenderOutput::Greyscale(bm.clone());
let extracted: Option<Bitmap> = out.into();
assert!(extracted.is_some());
let extracted = extracted.expect("greyscale should yield Some(Bitmap)");
assert_eq!(extracted.width, 4);
assert_eq!(extracted.pixels.len(), 16);
}
#[test]
fn render_output_into_bitmap_non_greyscale_is_none() {
let out = RenderOutput::Sdf {
width: 4,
height: 4,
data: vec![128u8; 16],
};
let extracted: Option<Bitmap> = out.into();
assert!(extracted.is_none());
let out2 = RenderOutput::Msdf {
width: 4,
height: 4,
data: vec![100u8; 48],
};
let extracted2: Option<Bitmap> = out2.into();
assert!(extracted2.is_none());
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip_bitmap() {
let bm = Bitmap {
width: 2,
height: 2,
pixels: vec![0, 128, 200, 255],
};
let json = serde_json::to_string(&bm).expect("serialize Bitmap");
let back: Bitmap = serde_json::from_str(&json).expect("deserialize Bitmap");
assert_eq!(back.width, bm.width);
assert_eq!(back.pixels, bm.pixels);
}
#[test]
fn test_decoration_rect_fields() {
let r = DecorationRect {
x: 1.0,
y: 2.0,
width: 10.0,
height: 1.5,
color: Rgba8 {
r: 0,
g: 0,
b: 0,
a: 255,
},
};
assert_eq!(r.width, 10.0);
assert_eq!(r.height, 1.5);
assert_eq!(r.color.a, 255);
}
#[test]
fn test_text_decoration_variants() {
let under = TextDecoration::Underline {
color: Rgba8::BLACK,
thickness: 1.0,
offset: 2.0,
};
let over = TextDecoration::Overline {
color: Rgba8::BLACK,
thickness: 1.0,
offset: 0.0,
};
let strike = TextDecoration::Strikethrough {
color: Rgba8::BLACK,
thickness: 1.5,
};
assert_ne!(under, over);
assert_ne!(under, strike);
let _copy = under;
let _copy2 = over;
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip_text_style() {
let style = TextStyle {
font_size: 24.0,
max_width: 600.0,
flow_direction: FlowDirection::Vertical,
alignment: TextAlignment::Center,
line_spacing: LineSpacing {
leading: 2.0,
line_height_multiplier: 1.5,
},
};
let json = serde_json::to_string(&style).expect("serialize TextStyle");
let back: TextStyle = serde_json::from_str(&json).expect("deserialize TextStyle");
assert_eq!(back.font_size, 24.0);
assert_eq!(back.alignment, TextAlignment::Center);
assert_eq!(back.flow_direction, FlowDirection::Vertical);
}
#[test]
fn test_bitmap_invert_coverage() {
let b = Bitmap {
width: 2,
height: 1,
pixels: vec![0u8, 255],
};
let inv = b.invert_coverage();
assert_eq!(inv.pixels[0], 255);
assert_eq!(inv.pixels[1], 0);
}
#[test]
fn test_bitmap_threshold() {
let b = Bitmap {
width: 3,
height: 1,
pixels: vec![64u8, 128, 200],
};
let t = b.threshold(128);
assert_eq!(t.pixels[0], 0);
assert_eq!(t.pixels[1], 255);
assert_eq!(t.pixels[2], 255);
}
#[test]
fn test_bitmap_tight_bounds_all_zero_returns_none() {
let b = Bitmap {
width: 4,
height: 4,
pixels: vec![0u8; 16],
};
assert!(b.tight_bounds().is_none());
}
#[test]
fn test_bitmap_tight_bounds_single_pixel() {
let mut pixels = vec![0u8; 16];
pixels[4 * 2 + 1] = 255; let b = Bitmap {
width: 4,
height: 4,
pixels,
};
let bounds = b.tight_bounds().expect("should find pixel");
assert_eq!(bounds, (1, 2, 1, 2));
}
#[test]
fn test_bitmap_crop() {
let pixels = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let b = Bitmap {
width: 4,
height: 4,
pixels,
};
let cropped = b.crop(1, 1, 2, 2);
assert_eq!(cropped.width, 2);
assert_eq!(cropped.height, 2);
assert_eq!(cropped.pixels, vec![6u8, 7, 10, 11]);
}
#[test]
fn test_bitmap_invert_is_involution() {
let b = Bitmap {
width: 3,
height: 1,
pixels: vec![10u8, 128, 200],
};
let double_inv = b.invert_coverage().invert_coverage();
assert_eq!(double_inv.pixels, b.pixels);
}
#[test]
fn test_bitmap_crop_out_of_bounds_fills_zero() {
let b = Bitmap {
width: 2,
height: 2,
pixels: vec![1u8, 2, 3, 4],
};
let cropped = b.crop(5, 5, 3, 3);
assert_eq!(cropped.pixels, vec![0u8; 9]);
}
#[test]
fn test_std_feature_enabled_by_default() {
#[cfg(feature = "std")]
{
let err: &dyn core::error::Error = &OxiTextError::InvalidFont;
let _ = err.to_string();
}
}
#[test]
fn test_vertical_position_effective_size() {
let vp = VerticalPosition::Superscript {
size_ratio: 0.6,
baseline_rise: 4.0,
};
assert!((vp.effective_size(16.0) - 9.6).abs() < 0.001);
}
#[test]
fn test_vertical_position_baseline_adjustment() {
let sub = VerticalPosition::Subscript {
size_ratio: 0.6,
baseline_drop: 3.0,
};
assert_eq!(sub.baseline_adjustment(16.0), -3.0);
let norm = VerticalPosition::Normal;
assert_eq!(norm.baseline_adjustment(16.0), 0.0);
}
}