use std::rc::Rc;
use crate::model::{RunProperties, UnderlineStyle};
use crate::render::dimension::Pt;
use crate::render::emoji::cluster::{EmojiPresentation, EmojiStructure};
use crate::render::fonts::TypefaceEntry;
use crate::render::geometry::PtSize;
use crate::render::resolve::color::RgbColor;
use crate::render::resolve::fonts::effective_font;
use crate::render::resolve::images::MediaEntry;
mod collect;
mod segment;
mod text;
pub use collect::{collect_fragments, FieldContext, FragmentCtx};
pub(super) const SUPERSCRIPT_FONT_SIZE_RATIO: f32 = 0.58;
pub(super) const SUPERSCRIPT_ASCENT_OFFSET_RATIO: f32 = 0.33;
pub(super) const SUBSCRIPT_HEIGHT_OFFSET_RATIO: f32 = 0.08;
#[derive(Clone, Debug)]
pub struct FontProps {
pub family: Rc<str>,
pub size: Pt,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub char_spacing: Pt,
pub text_scale: f32,
pub underline_position: Pt,
pub underline_thickness: Pt,
}
#[derive(Clone, Copy, Debug)]
pub struct TextMetrics {
pub ascent: Pt,
pub descent: Pt,
pub leading: Pt,
}
impl TextMetrics {
pub fn height(&self) -> Pt {
self.ascent + self.descent
}
pub fn line_height(&self) -> Pt {
self.ascent + self.descent + self.leading
}
}
#[derive(Clone, Copy, Debug)]
pub struct FragmentBorder {
pub width: Pt,
pub color: RgbColor,
pub space: Pt,
}
#[derive(Clone, Debug)]
pub enum Fragment {
Text {
text: Rc<str>,
font: FontProps,
color: RgbColor,
shading: Option<RgbColor>,
border: Option<FragmentBorder>,
width: Pt,
trimmed_width: Pt,
metrics: TextMetrics,
hyperlink_url: Option<String>,
baseline_offset: Pt,
text_offset: Pt,
},
Image {
size: PtSize,
rel_id: String,
image_data: Option<MediaEntry>,
},
Emoji {
text: String,
typeface: TypefaceEntry,
size: Pt,
presentation: EmojiPresentation,
structure: EmojiStructure,
advance: Pt,
metrics: TextMetrics,
line_metrics: TextMetrics,
baseline_offset: Pt,
},
Tab {
line_height: Pt,
fitting_width: Option<Pt>,
},
LineBreak {
line_height: Pt,
},
ColumnBreak,
PageBreak {
line_height: Pt,
},
Bookmark {
name: String,
},
}
impl Fragment {
pub fn width(&self) -> Pt {
match self {
Fragment::Text { width, .. } => *width,
Fragment::Image { size, .. } => size.width,
Fragment::Emoji { advance, .. } => *advance,
Fragment::Tab { fitting_width, .. } => fitting_width.unwrap_or(MIN_TAB_WIDTH),
Fragment::LineBreak { .. }
| Fragment::ColumnBreak
| Fragment::PageBreak { .. }
| Fragment::Bookmark { .. } => Pt::ZERO,
}
}
pub fn trimmed_width(&self) -> Pt {
match self {
Fragment::Text { trimmed_width, .. } => *trimmed_width,
other => other.width(),
}
}
pub fn height(&self) -> Pt {
match self {
Fragment::Text { metrics, .. } => metrics.height(),
Fragment::Image { size, .. } => size.height,
Fragment::Emoji { line_metrics, .. } => line_metrics.height(),
Fragment::Tab { line_height, .. }
| Fragment::LineBreak { line_height }
| Fragment::PageBreak { line_height } => *line_height,
Fragment::ColumnBreak | Fragment::Bookmark { .. } => Pt::ZERO,
}
}
pub fn is_line_break(&self) -> bool {
matches!(
self,
Fragment::LineBreak { .. } | Fragment::ColumnBreak | Fragment::PageBreak { .. }
)
}
pub fn is_page_break(&self) -> bool {
matches!(self, Fragment::PageBreak { .. })
}
pub fn font_props(&self) -> Option<&FontProps> {
match self {
Fragment::Text { font, .. } => Some(font),
_ => None,
}
}
}
pub const MIN_TAB_WIDTH: Pt = Pt::new(1.0);
pub fn font_props_from_run(
rp: &RunProperties,
default_family: &str,
default_size: Pt,
) -> FontProps {
let family = effective_font(&rp.fonts).unwrap_or(default_family);
let size = rp.font_size.map(Pt::from).unwrap_or(default_size);
let char_spacing = rp.spacing.map(Pt::from).unwrap_or(Pt::ZERO);
let text_scale = rp.text_scale.map_or(1.0, |s| s.as_factor());
FontProps {
family: Rc::from(family),
size,
bold: rp.bold.unwrap_or(false),
italic: rp.italic.unwrap_or(false),
underline: matches!(rp.underline, Some(s) if s != UnderlineStyle::None),
char_spacing,
text_scale,
underline_position: Pt::ZERO,
underline_thickness: Pt::ZERO,
}
}
pub fn to_roman_lower(mut n: u32) -> String {
const VALS: [(u32, &str); 13] = [
(1000, "m"),
(900, "cm"),
(500, "d"),
(400, "cd"),
(100, "c"),
(90, "xc"),
(50, "l"),
(40, "xl"),
(10, "x"),
(9, "ix"),
(5, "v"),
(4, "iv"),
(1, "i"),
];
let mut s = String::new();
for &(val, sym) in &VALS {
while n >= val {
s.push_str(sym);
n -= val;
}
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::UnderlineStyle;
#[test]
fn font_props_default_fallback() {
let rp = RunProperties::default();
let fp = font_props_from_run(&rp, "Helvetica", Pt::new(12.0));
assert_eq!(&*fp.family, "Helvetica");
assert_eq!(fp.size.raw(), 12.0);
assert!(!fp.bold);
assert!(!fp.italic);
}
fn rp_with_underline(style: Option<UnderlineStyle>) -> RunProperties {
RunProperties {
underline: style,
..RunProperties::default()
}
}
#[test]
fn font_props_underline_absent_is_false() {
let fp = font_props_from_run(&rp_with_underline(None), "Helvetica", Pt::new(12.0));
assert!(!fp.underline, "no <w:u> element → no underline");
}
#[test]
fn font_props_underline_explicit_none_is_false() {
let fp = font_props_from_run(
&rp_with_underline(Some(UnderlineStyle::None)),
"Helvetica",
Pt::new(12.0),
);
assert!(
!fp.underline,
"<w:u w:val=\"none\"/> is the spec's explicit \"no underline\" \
override; font_props.underline must remain false"
);
}
#[test]
fn font_props_underline_single_is_true() {
let fp = font_props_from_run(
&rp_with_underline(Some(UnderlineStyle::Single)),
"Helvetica",
Pt::new(12.0),
);
assert!(fp.underline, "<w:u w:val=\"single\"/> → underline drawn");
}
#[test]
fn font_props_text_scale_default_is_one() {
let fp = font_props_from_run(&RunProperties::default(), "Helvetica", Pt::new(12.0));
assert_eq!(fp.text_scale, 1.0);
}
#[test]
fn font_props_text_scale_compressed() {
let rp = RunProperties {
text_scale: Some(crate::model::TextScale::new(80)),
..RunProperties::default()
};
let fp = font_props_from_run(&rp, "Helvetica", Pt::new(12.0));
assert!((fp.text_scale - 0.8).abs() < f32::EPSILON);
}
#[test]
fn font_props_text_scale_expanded() {
let rp = RunProperties {
text_scale: Some(crate::model::TextScale::new(150)),
..RunProperties::default()
};
let fp = font_props_from_run(&rp, "Helvetica", Pt::new(12.0));
assert!((fp.text_scale - 1.5).abs() < f32::EPSILON);
}
#[test]
fn font_props_underline_double_is_true() {
let fp = font_props_from_run(
&rp_with_underline(Some(UnderlineStyle::Double)),
"Helvetica",
Pt::new(12.0),
);
assert!(fp.underline);
}
}