use std::collections::HashMap;
use std::rc::Rc;
use crate::model::{self, FirstLineIndent, LineSpacing};
use crate::render::dimension::Pt;
use crate::render::geometry::PtSize;
use crate::render::layout::fragment::Fragment;
use crate::render::layout::measurer::TextMeasurer;
use crate::render::layout::paragraph::TabStopDef;
use crate::render::layout::paragraph::{
BorderLine, LineSpacingRule, ParagraphBorderStyle, ParagraphStyle,
};
use crate::render::layout::table::{
CellBorderOverride, TableBorderConfig, TableBorderLine, TableBorderStyle,
};
use crate::render::resolve::color::{resolve_color, ColorContext, RgbColor};
use crate::render::resolve::fonts::effective_font;
use crate::render::resolve::images::MediaEntry;
use crate::render::resolve::properties::merge_paragraph_properties;
use crate::render::resolve::ResolvedDocument;
use super::{BuildContext, SPEC_DEFAULT_FONT_SIZE, SPEC_FALLBACK_FONT};
pub(super) fn resolve_paragraph_defaults(
para: &model::Paragraph,
resolved: &ResolvedDocument,
defer_doc_defaults: bool,
) -> (
String,
Pt,
RgbColor,
model::ParagraphProperties,
model::RunProperties,
) {
let mut para_props = para.properties.clone();
let mut run_defaults = resolved.doc_defaults_run.clone();
let mut default_family = resolved
.theme
.as_ref()
.map(|t| t.minor_font.latin.as_str())
.filter(|s| !s.is_empty())
.unwrap_or(SPEC_FALLBACK_FONT)
.to_string();
let mut default_size = resolved
.doc_defaults_run
.font_size
.map(Pt::from)
.unwrap_or(SPEC_DEFAULT_FONT_SIZE);
let mut default_color = RgbColor::BLACK;
let effective_style_id = para
.style_id
.as_ref()
.or(resolved.default_paragraph_style_id.as_ref());
if let Some(style_id) = effective_style_id {
if let Some(resolved_style) = resolved.styles.get(style_id) {
merge_paragraph_properties(&mut para_props, &resolved_style.paragraph);
run_defaults = resolved_style.run.clone();
}
}
if !defer_doc_defaults {
merge_paragraph_properties(&mut para_props, &resolved.doc_defaults_paragraph);
}
if let Some(f) = effective_font(&run_defaults.fonts) {
default_family = f.to_string();
}
if let Some(fs) = run_defaults.font_size {
default_size = Pt::from(fs);
}
if let Some(c) = run_defaults.color {
default_color = resolve_color(c, ColorContext::Text);
}
(
default_family,
default_size,
default_color,
para_props,
run_defaults,
)
}
pub(super) fn doc_font_family(ctx: &BuildContext) -> String {
ctx.resolved
.theme
.as_ref()
.map(|t| t.minor_font.latin.as_str())
.filter(|s| !s.is_empty())
.unwrap_or(SPEC_FALLBACK_FONT)
.to_string()
}
pub(super) fn doc_font_size(ctx: &BuildContext) -> Pt {
ctx.resolved
.doc_defaults_run
.font_size
.map(Pt::from)
.unwrap_or(SPEC_DEFAULT_FONT_SIZE)
}
pub(super) fn paragraph_style_from_props(props: &model::ParagraphProperties) -> ParagraphStyle {
let indent_left = props
.indentation
.and_then(|i| i.start)
.map(Pt::from)
.unwrap_or(Pt::ZERO);
let indent_right = props
.indentation
.and_then(|i| i.end)
.map(Pt::from)
.unwrap_or(Pt::ZERO);
let indent_first_line = props
.indentation
.and_then(|i| i.first_line)
.map(|fl| match fl {
FirstLineIndent::FirstLine(v) => Pt::from(v),
FirstLineIndent::Hanging(v) => -Pt::from(v),
FirstLineIndent::None => Pt::ZERO,
})
.unwrap_or(Pt::ZERO);
let space_before = if props.spacing.and_then(|s| s.before_auto_spacing) == Some(true) {
Pt::new(14.0)
} else {
props
.spacing
.and_then(|s| s.before)
.map(Pt::from)
.unwrap_or(Pt::ZERO)
};
let space_after = if props.spacing.and_then(|s| s.after_auto_spacing) == Some(true) {
Pt::new(14.0)
} else {
props
.spacing
.and_then(|s| s.after)
.map(Pt::from)
.unwrap_or(Pt::ZERO)
};
let line_spacing = props
.spacing
.and_then(|s| s.line)
.map(|ls| match ls {
LineSpacing::Auto(v) => LineSpacingRule::Auto(Pt::from(v).raw() / 12.0),
LineSpacing::Exact(v) => LineSpacingRule::Exact(Pt::from(v)),
LineSpacing::AtLeast(v) => LineSpacingRule::AtLeast(Pt::from(v)),
})
.unwrap_or(LineSpacingRule::Auto(1.0));
let tabs: Vec<TabStopDef> = props
.tabs
.iter()
.filter(|t| t.alignment != model::TabAlignment::Clear)
.map(|t| TabStopDef {
position: Pt::from(t.position),
alignment: t.alignment,
leader: t.leader,
})
.collect();
ParagraphStyle {
alignment: props.alignment.unwrap_or(model::Alignment::Start),
space_before,
space_after,
indent_left,
indent_right,
indent_first_line,
line_spacing,
tabs,
drop_cap: None,
borders: resolve_paragraph_borders(props),
shading: props
.shading
.as_ref()
.map(|s| resolve_color(s.fill, ColorContext::Background)),
keep_next: props.keep_next.unwrap_or(false),
contextual_spacing: props.contextual_spacing.unwrap_or(false),
style_id: None, page_floats: Vec::new(),
page_y: crate::render::dimension::Pt::ZERO,
page_x: crate::render::dimension::Pt::ZERO,
page_content_width: crate::render::dimension::Pt::ZERO,
}
}
fn resolve_paragraph_borders(props: &model::ParagraphProperties) -> Option<ParagraphBorderStyle> {
let pbdr = props.borders.as_ref()?;
let convert = |b: &model::Border| -> BorderLine {
BorderLine {
width: Pt::from(b.width),
color: resolve_color(b.color, ColorContext::Text),
space: Pt::from(b.space),
}
};
let style = ParagraphBorderStyle {
top: pbdr.top.as_ref().map(convert),
bottom: pbdr.bottom.as_ref().map(convert),
left: pbdr.left.as_ref().map(convert),
right: pbdr.right.as_ref().map(convert),
};
if style.top.is_some()
|| style.bottom.is_some()
|| style.left.is_some()
|| style.right.is_some()
{
Some(style)
} else {
None
}
}
fn convert_model_border(b: &model::Border) -> TableBorderLine {
TableBorderLine {
width: Pt::from(b.width),
color: resolve_color(b.color, ColorContext::Text),
style: match b.style {
model::BorderStyle::Double => TableBorderStyle::Double,
_ => TableBorderStyle::Single,
},
}
}
pub(super) fn convert_cell_border_override(
b: &Option<model::Border>,
) -> Option<CellBorderOverride> {
b.as_ref().map(|b| {
if b.style == model::BorderStyle::None {
CellBorderOverride::Nil
} else {
CellBorderOverride::Border(convert_model_border(b))
}
})
}
pub(super) fn merge_table_borders(
direct: &model::TableBorders,
style: &model::TableBorders,
) -> model::TableBorders {
model::TableBorders {
top: direct.top.or(style.top),
bottom: direct.bottom.or(style.bottom),
left: direct.left.or(style.left),
right: direct.right.or(style.right),
inside_h: direct.inside_h.or(style.inside_h),
inside_v: direct.inside_v.or(style.inside_v),
}
}
pub(super) fn convert_table_border_config(b: &model::TableBorders) -> TableBorderConfig {
let convert = |border: &Option<model::Border>| -> Option<TableBorderLine> {
border.as_ref().and_then(|b| {
if b.style == model::BorderStyle::None {
None
} else {
Some(convert_model_border(b))
}
})
};
TableBorderConfig {
top: convert(&b.top),
bottom: convert(&b.bottom),
left: convert(&b.left),
right: convert(&b.right),
inside_h: convert(&b.inside_h),
inside_v: convert(&b.inside_v),
}
}
pub(super) fn split_oversized_fragments(
fragments: Vec<Fragment>,
max_width: Pt,
ctx: &BuildContext,
) -> Vec<Fragment> {
if max_width <= Pt::ZERO {
return fragments;
}
let mut result = Vec::with_capacity(fragments.len());
for frag in fragments {
match &frag {
Fragment::Text {
text, width, font, ..
} if *width > max_width && text.chars().count() > 1 => {
for ch in text.chars() {
let ch_str = ch.to_string();
let (w, m) = ctx.measurer.measure(&ch_str, font);
if let Fragment::Text {
color,
shading,
border,
hyperlink_url,
baseline_offset,
..
} = &frag
{
result.push(Fragment::Text {
text: Rc::from(ch_str.as_str()),
font: font.clone(),
color: *color,
shading: *shading,
border: *border,
width: w,
trimmed_width: w,
metrics: m,
hyperlink_url: hyperlink_url.clone(),
baseline_offset: *baseline_offset,
text_offset: Pt::ZERO,
});
}
}
}
_ => result.push(frag),
}
}
result
}
pub(super) fn populate_image_data(
fragments: &mut [Fragment],
media: &HashMap<model::RelId, MediaEntry>,
) {
for frag in fragments.iter_mut() {
if let Fragment::Image {
rel_id, image_data, ..
} = frag
{
if image_data.is_none() {
if let Some(entry) = media.get(&model::RelId::new(rel_id.as_str())) {
*image_data = Some(entry.clone());
}
}
}
}
}
pub(super) fn remap_legacy_font_chars(
text: &str,
font_family: &str,
fallback_family: &str,
) -> (String, String) {
let is_symbol = font_family.eq_ignore_ascii_case("Symbol");
let is_wingdings = font_family.eq_ignore_ascii_case("Wingdings");
if !is_symbol && !is_wingdings {
let family = if font_family.is_empty() {
fallback_family
} else {
font_family
};
return (text.to_string(), family.to_string());
}
let remapped: String = text
.chars()
.map(|ch| {
let code = ch as u32;
if is_symbol && (0xF020..=0xF0FF).contains(&code) {
match code {
0xF020 => '\u{0020}', 0xF021 => '\u{0021}', 0xF025 => '\u{0025}', 0xF028 => '\u{0028}', 0xF029 => '\u{0029}', 0xF02B => '\u{002B}', 0xF02E => '\u{002E}', 0xF030..=0xF039 => char::from_u32(code - 0xF000).unwrap_or(ch), 0xF03C => '\u{003C}', 0xF03D => '\u{003D}', 0xF03E => '\u{003E}', 0xF05B => '\u{005B}', 0xF05D => '\u{005D}', 0xF07B => '\u{007B}', 0xF07C => '\u{007C}', 0xF07D => '\u{007D}', 0xF07E => '\u{223C}', 0xF0A0 => '\u{20AC}', 0xF0A5 => '\u{221E}', 0xF0A7 => '\u{2663}', 0xF0A8 => '\u{2666}', 0xF0A9 => '\u{2665}', 0xF0AA => '\u{2660}', 0xF0AB => '\u{2194}', 0xF0AC => '\u{2190}', 0xF0AD => '\u{2191}', 0xF0AE => '\u{2192}', 0xF0AF => '\u{2193}', 0xF0B0 => '\u{00B0}', 0xF0B1 => '\u{00B1}', 0xF0B2 => '\u{2033}', 0xF0B3 => '\u{2265}', 0xF0B4 => '\u{00D7}', 0xF0B5 => '\u{221D}', 0xF0B7 => '\u{2022}', 0xF0B8 => '\u{00F7}', 0xF0B9 => '\u{2260}', 0xF0BA => '\u{2261}', 0xF0BB => '\u{2248}', 0xF0BC => '\u{2026}', 0xF0C0 => '\u{2135}', 0xF0C1 => '\u{2111}', 0xF0C2 => '\u{211C}', 0xF0C3 => '\u{2118}', 0xF0C5 => '\u{2297}', 0xF0C6 => '\u{2295}', 0xF0C7 => '\u{2205}', 0xF0C8 => '\u{2229}', 0xF0C9 => '\u{222A}', 0xF0CB => '\u{2283}', 0xF0CC => '\u{2287}', 0xF0CD => '\u{2284}', 0xF0CE => '\u{2282}', 0xF0CF => '\u{2286}', 0xF0D0 => '\u{2208}', 0xF0D1 => '\u{2209}', 0xF0D5 => '\u{220F}', 0xF0D6 => '\u{221A}', 0xF0D7 => '\u{22C5}', 0xF0D8 => '\u{00AC}', 0xF0D9 => '\u{2227}', 0xF0DA => '\u{2228}', 0xF0E0 => '\u{21D0}', 0xF0E1 => '\u{21D1}', 0xF0E2 => '\u{21D2}', 0xF0E3 => '\u{21D3}', 0xF0E4 => '\u{21D4}', 0xF0E5 => '\u{2329}', 0xF0F1 => '\u{232A}', 0xF0F2 => '\u{222B}', _ => ch,
}
} else if is_wingdings && (0xF020..=0xF0FF).contains(&code) {
match code {
0xF021 => '\u{270E}', 0xF022 => '\u{2702}', 0xF023 => '\u{2701}', 0xF028 => '\u{1F4CB}', 0xF029 => '\u{1F4CB}', 0xF041 => '\u{FE4E}', 0xF046 => '\u{1F44D}', 0xF04C => '\u{2639}', 0xF04A => '\u{263A}', 0xF06C => '\u{25CF}', 0xF06D => '\u{274D}', 0xF06E => '\u{25A0}', 0xF06F => '\u{25A1}', 0xF070 => '\u{25A1}', 0xF071 => '\u{2751}', 0xF072 => '\u{2752}', 0xF073 => '\u{25C6}', 0xF074 => '\u{2756}', 0xF076 => '\u{2756}', 0xF09F => '\u{2708}', 0xF0A1 => '\u{270C}', 0xF0A4 => '\u{261C}', 0xF0A5 => '\u{261E}', 0xF0A7 => '\u{25AA}', 0xF0A8 => '\u{25FB}', 0xF0D5 => '\u{232B}', 0xF0D8 => '\u{27A2}', 0xF0E8 => '\u{2B22}', 0xF0F0 => '\u{2B1A}', 0xF0FC => '\u{2714}', 0xF0FB => '\u{2718}', 0xF0FE => '\u{2612}', _ => ch,
}
} else {
ch
}
})
.collect();
(remapped, fallback_family.to_string())
}
pub(super) fn populate_underline_metrics(fragments: &mut [Fragment], measurer: &TextMeasurer) {
for frag in fragments.iter_mut() {
if let Fragment::Text { font, .. } = frag {
if font.underline {
let (pos, thickness) = measurer.underline_metrics(font);
font.underline_position = pos;
font.underline_thickness = thickness;
}
}
}
}
pub(super) fn pic_bullet_size(bullet: &model::NumPicBullet) -> PtSize {
use crate::model::VmlLengthUnit;
let default = PtSize::new(Pt::new(9.0), Pt::new(9.0));
let shape = match bullet.pict.as_ref().and_then(|p| p.shapes.first()) {
Some(s) => s,
None => return default,
};
let to_pt = |len: &crate::model::VmlLength| -> Pt {
let val = len.value as f32;
match len.unit {
VmlLengthUnit::Pt => Pt::new(val),
VmlLengthUnit::In => Pt::new(val * 72.0),
VmlLengthUnit::Cm => Pt::new(val * 28.3465),
VmlLengthUnit::Mm => Pt::new(val * 2.83465),
VmlLengthUnit::Px => Pt::new(val * 0.75),
_ => Pt::new(val),
}
};
let w = shape
.style
.width
.as_ref()
.map(to_pt)
.unwrap_or(default.width);
let h = shape
.style
.height
.as_ref()
.map(to_pt)
.unwrap_or(default.height);
PtSize::new(w, h)
}