use std::collections::{HashMap, HashSet};
use pdf_writer::{Pdf, Ref};
use crate::fonts::{FontEntry, font_key_buf, register_font};
use crate::model::{
Block, Document, FieldCode, Paragraph, Run,
};
use super::header_footer::hf_paragraphs;
use super::{collect_paras, label_font_key, para_runs_with_textboxes};
pub(super) fn collect_all_runs(doc: &Document) -> Vec<&Run> {
let hf_runs = doc.sections.iter().flat_map(|s| {
[
&s.properties.header_default,
&s.properties.header_first,
&s.properties.header_even,
&s.properties.footer_default,
&s.properties.footer_first,
&s.properties.footer_even,
]
.into_iter()
.filter_map(|hf| hf.as_ref())
.flat_map(|hf| hf_paragraphs(hf))
.flat_map(|p| para_runs_with_textboxes(p))
});
let footnote_runs = doc
.footnotes
.values()
.flat_map(|fn_| fn_.paragraphs.iter())
.flat_map(|p| para_runs_with_textboxes(p));
let endnote_runs = doc
.endnotes
.values()
.flat_map(|en| en.paragraphs.iter())
.flat_map(|p| para_runs_with_textboxes(p));
doc.sections
.iter()
.flat_map(|s| s.blocks.iter())
.flat_map(|block| -> Vec<&Run> {
match block {
Block::Paragraph(para) => para_runs_with_textboxes(para),
Block::Table(table) => table
.rows
.iter()
.flat_map(|row| row.cells.iter())
.flat_map(|cell| cell.all_paragraphs())
.flat_map(|para| para_runs_with_textboxes(para))
.collect(),
}
})
.chain(hf_runs)
.chain(footnote_runs)
.chain(endnote_runs)
.collect()
}
fn collect_used_chars(doc: &Document, all_runs: &[&Run]) -> HashMap<String, HashSet<char>> {
let mut used: HashMap<String, HashSet<char>> = HashMap::new();
let mut key_buf = String::new();
for run in all_runs {
let key = font_key_buf(run, &mut key_buf);
let chars = used.entry(key.to_string()).or_default();
if run.caps || run.small_caps {
chars.extend(run.text.to_uppercase().chars());
} else {
chars.extend(run.text.chars());
}
if let Some(ref fc) = run.field_code {
match fc {
FieldCode::Page | FieldCode::NumPages | FieldCode::PageRef(_) => {
chars.extend('0'..='9');
}
FieldCode::StyleRef(_) => {}
}
}
if run.footnote_id.is_some()
|| run.is_footnote_ref_mark
|| run.endnote_id.is_some()
|| run.is_endnote_ref_mark
{
chars.extend('0'..='9');
}
}
let mut all_paras: Vec<&Paragraph> = doc
.sections
.iter()
.flat_map(|s| s.blocks.iter())
.flat_map(|block| -> Vec<&Paragraph> {
match block {
Block::Paragraph(p) => collect_paras(p),
Block::Table(t) => t
.rows
.iter()
.flat_map(|row| row.cells.iter())
.flat_map(|cell| cell.all_paragraphs())
.flat_map(|p| collect_paras(p))
.collect(),
}
})
.collect();
for note in doc.footnotes.values().chain(doc.endnotes.values()) {
for p in ¬e.paragraphs {
all_paras.extend(collect_paras(p));
}
}
for para in &all_paras {
if !para.list_label.is_empty() {
if let Some(key) = label_font_key(para) {
used.entry(key)
.or_default()
.extend(para.list_label.chars());
}
if let Some(run) = para.runs.first() {
let body_key = font_key_buf(run, &mut key_buf).to_string();
let entry = used.entry(body_key).or_default();
for c in para.list_label.chars() {
if let Some(sub) = super::list_label::symbol_pua_to_unicode(c) {
entry.insert(sub);
}
}
}
}
for stop in ¶.tab_stops {
if let Some(leader_char) = stop.leader
&& let Some(run) = para.runs.first()
{
let key = font_key_buf(run, &mut key_buf).to_string();
used.entry(key).or_default().insert(leader_char);
}
}
}
if let Some(first_run) = all_runs.first() {
let sa_key = font_key_buf(first_run, &mut key_buf).to_string();
let chars = used.entry(sa_key).or_default();
for para in &all_paras {
for diagram in ¶.smartart {
for shape in &diagram.shapes {
for sa_para in &shape.paragraphs {
for run in &sa_para.runs {
if run.font_name.is_none() {
chars.extend(run.text.chars());
}
}
if let Some(ref b) = sa_para.bullet {
chars.extend(b.chars());
}
}
}
}
}
}
if !doc.comments.is_empty() {
let body_run = all_runs
.iter()
.find(|r| !r.bold && !r.italic)
.or_else(|| all_runs.first())
.copied();
if let Some(body_run) = body_run {
let mut reg = (*body_run).clone();
reg.bold = false;
reg.italic = false;
let reg_key = font_key_buf(®, &mut key_buf).to_string();
let mut bold = (*body_run).clone();
bold.bold = true;
bold.italic = false;
let bold_key = font_key_buf(&bold, &mut key_buf).to_string();
let regular = used.entry(reg_key).or_default();
for comment in doc.comments.values() {
regular.extend(comment.text.chars());
}
let bold = used.entry(bold_key).or_default();
for comment in doc.comments.values() {
bold.extend("Commented []: ".chars());
bold.extend(comment.initials.chars());
bold.extend(comment.display_index.to_string().chars());
}
}
}
let mut styleref_chars: HashSet<char> = HashSet::new();
for para in &all_paras {
if para.style_id.is_some() {
for run in ¶.runs {
styleref_chars.extend(run.text.chars());
}
}
for run in ¶.runs {
if run.char_style_id.is_some() {
styleref_chars.extend(run.text.chars());
}
}
}
let all_page_num_formats: Vec<&str> = doc
.sections
.iter()
.filter_map(|s| s.properties.page_num_format.as_deref())
.collect();
for section in &doc.sections {
for hf in [
§ion.properties.header_default,
§ion.properties.header_first,
§ion.properties.header_even,
§ion.properties.footer_default,
§ion.properties.footer_first,
§ion.properties.footer_even,
]
.into_iter()
.flatten()
{
for para in hf_paragraphs(hf) {
for run in ¶.runs {
let key = font_key_buf(run, &mut key_buf);
let chars = used.entry(key.to_string()).or_default();
if run.caps || run.small_caps {
chars.extend(run.text.to_uppercase().chars());
} else {
chars.extend(run.text.chars());
}
if let Some(ref fc) = run.field_code {
match fc {
FieldCode::Page | FieldCode::NumPages | FieldCode::PageRef(_) => {
chars.extend('0'..='9');
for fmt in &all_page_num_formats {
extend_chars_for_num_format(chars, fmt);
}
}
FieldCode::StyleRef(_) => {
chars.extend(styleref_chars.iter());
}
}
}
}
}
}
}
{
let mut chart_label_chars: HashSet<char> = HashSet::new();
for para in &all_paras {
if let Some(ref ic) = para.inline_chart {
chart_label_chars.extend('0'..='9');
chart_label_chars.insert('.');
chart_label_chars.insert('-');
chart_label_chars.insert(',');
chart_label_chars.insert('%');
for series in &ic.chart.series {
chart_label_chars.extend(series.label.chars());
}
if let Some(ref cat_axis) = ic.chart.cat_axis {
for label in &cat_axis.labels {
chart_label_chars.extend(label.chars());
}
}
}
}
if !chart_label_chars.is_empty() {
used.entry(doc.chart_font_name.clone())
.or_default()
.extend(chart_label_chars);
}
}
for chars in used.values_mut() {
chars.insert(' ');
}
used
}
fn extend_chars_for_num_format(chars: &mut HashSet<char>, fmt: &str) {
match fmt {
"lowerRoman" => chars.extend(['i', 'v', 'x', 'l', 'c', 'd', 'm']),
"upperRoman" => chars.extend(['I', 'V', 'X', 'L', 'C', 'D', 'M']),
"lowerLetter" => chars.extend('a'..='z'),
"upperLetter" => chars.extend('A'..='Z'),
_ => {}
}
}
pub(super) fn smartart_font_key_str(name: &str, bold: bool, italic: bool) -> String {
let mut key = crate::fonts::primary_font_name(name).to_string();
match (bold, italic) {
(true, true) => key.push_str("/BI"),
(true, false) => key.push_str("/B"),
(false, true) => key.push_str("/I"),
(false, false) => {}
}
key
}
pub(super) fn collect_and_register_fonts(
doc: &Document,
pdf: &mut Pdf,
alloc: &mut impl FnMut() -> Ref,
) -> (HashMap<String, FontEntry>, Vec<String>) {
let mut seen_fonts: HashMap<String, FontEntry> = HashMap::new();
let mut font_order: Vec<String> = Vec::new();
let all_runs = collect_all_runs(doc);
let mut used_chars_per_font = collect_used_chars(doc, &all_runs);
for section in &doc.sections {
for block in §ion.blocks {
if let Block::Paragraph(para) = block {
for diagram in ¶.smartart {
for shape in &diagram.shapes {
for sa_para in &shape.paragraphs {
for (i, run) in sa_para.runs.iter().enumerate() {
if let Some(ref name) = run.font_name {
let key = smartart_font_key_str(name, run.bold, run.italic);
let chars = used_chars_per_font.entry(key).or_default();
chars.extend(run.text.chars());
if i == 0 {
if let Some(ref b) = sa_para.bullet {
chars.extend(b.chars());
chars.insert(' ');
}
}
}
}
}
}
}
}
}
}
let mut key_buf = String::new();
for run in &all_runs {
let key = font_key_buf(run, &mut key_buf);
if !seen_fonts.contains_key(key) {
let key_owned = key.to_string();
let pdf_name = format!("F{}", font_order.len() + 1);
let used = used_chars_per_font
.get(&key_owned)
.cloned()
.unwrap_or_default();
let entry = register_font(
pdf,
&run.font_name,
run.bold,
run.italic,
pdf_name,
alloc,
&doc.embedded_fonts,
&used,
&doc.font_table,
);
font_order.push(key_owned.clone());
seen_fonts.insert(key_owned, entry);
}
}
for (key, used) in &used_chars_per_font {
if seen_fonts.contains_key(key) {
continue;
}
let (base, bold, italic) = if let Some((name, suffix)) = key.split_once('/') {
let b = suffix.contains('B');
let i = suffix.contains('I');
(name, b, i)
} else {
(key.as_str(), false, false)
};
let pdf_name = format!("F{}", font_order.len() + 1);
let entry = register_font(
pdf, base, bold, italic, pdf_name, alloc,
&doc.embedded_fonts, used, &doc.font_table,
);
seen_fonts.insert(key.clone(), entry);
font_order.push(key.clone());
}
let mut all_missing_cjk: HashSet<char> = HashSet::new();
for entry in seen_fonts.values() {
all_missing_cjk.extend(&entry.missing_cjk_chars);
}
if !all_missing_cjk.is_empty() {
let fallback_key = "__cjk_fallback".to_string();
let pdf_name = format!("F{}", font_order.len() + 1);
#[cfg(target_os = "macos")]
let fallback_font_name =
"PMingLiU;MingLiU;Songti TC;Hiragino Sans GB;Hiragino Sans GB W3;Hiragino Sans W3;Hiragino Kaku Gothic ProN W3;Arial Unicode MS;Malgun Gothic";
#[cfg(target_os = "linux")]
let fallback_font_name = "Noto Sans CJK SC;Noto Sans CJK KR;Noto Sans CJK JP";
#[cfg(target_os = "windows")]
let fallback_font_name = "Yu Gothic;Microsoft YaHei;Malgun Gothic";
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let fallback_font_name = "Arial Unicode MS";
let entry = register_font(
pdf,
&fallback_font_name,
false,
false,
pdf_name,
alloc,
&doc.embedded_fonts,
&all_missing_cjk,
&doc.font_table,
);
font_order.push(fallback_key.clone());
seen_fonts.insert(fallback_key, entry);
if let Some(fb_widths) = seen_fonts
.get("__cjk_fallback")
.and_then(|e| e.char_widths_1000.clone())
{
for entry in seen_fonts.values_mut() {
if entry.missing_cjk_chars.is_empty() {
continue;
}
let widths = entry.char_widths_1000.get_or_insert_with(HashMap::new);
for &ch in &entry.missing_cjk_chars {
if let Some(&w) = fb_widths.get(&ch) {
widths.insert(ch, w);
}
}
}
}
}
if seen_fonts.is_empty() {
let pdf_name = "F1".to_string();
let entry = register_font(
pdf,
"Helvetica",
false,
false,
pdf_name,
alloc,
&doc.embedded_fonts,
&HashSet::new(),
&doc.font_table,
);
seen_fonts.insert("Helvetica".to_string(), entry);
font_order.push("Helvetica".to_string());
}
(seen_fonts, font_order)
}