use super::boxes::{BoxInput, layout_box}; use super::cache::{Totals, hash_of}; use super::ctx::{Ctx, with_pct_base, with_pin}; use super::flow::again; use super::page::{PageBuilder, PageFrame, page_border_rules}; use super::placed::{HfSlot, Placed}; use super::{ Arc, Document, HashMap, HashSet, HeaderSet, InlineObject, Page, Path, Rect, SectionProps, SectionStart, StoryRef, ViewMode, fields, styleref, work, }; /// Whether a section that starts on a new page needs a blank page first so it lands on the right /// side, and the page number it restarts at. `last` is the number of the page before it and /// `pages` how many pages there are so far. /// - An odd- or even-page break counts on from the previous section's numbers. A section that /// restarts its numbering gets no blank page unless the document prints both sides (different /// odd and even headers, or mirror margins); a restart at the wrong parity takes the next number. /// - With different odd and even headers, a next-page break that restarts the numbers at an odd /// (even) number starts on an odd (even) sheet. /// /// (Word's output for docxide-pdf's fixtures: bush_fires_act_comparison has a blank page 2.) pub(crate) fn section_side(start: SectionStart, restart: Option, last: u32, pages: usize, even_odd: bool, mirror: bool) -> (bool, Option) { match start { SectionStart::OddPage | SectionStart::EvenPage => { let want_odd = start == SectionStart::OddPage; let continuing_odd = last.saturating_add(1) % 2 == 1; let filler = pages > 0 && continuing_odd != want_odd && (restart.is_none() || even_odd || mirror); let restart = restart.map(|n| if (n % 2 == 1) == want_odd { n } else { n.saturating_add(1) }); (filler, restart) } SectionStart::NextPage => { let filler = even_odd && pages > 0 && restart.is_some_and(|n| (n % 2 == 1) != ((pages + 1) % 2 == 1)); (filler, restart) } SectionStart::Continuous | SectionStart::NextColumn => (false, restart), } } /// The document's sections with their headers and footers (Word's "link to previous"), each /// section's resolved once for the whole layout in document order rather than by looking back /// over the sections before it whenever one is needed. pub(crate) struct HfLinks<'a> { pub(crate) sections: Vec<(usize, &'a SectionProps)>, pub(crate) heads: Linked, pub(crate) feet: Linked, } impl<'a> HfLinks<'a> { pub(crate) fn new(sections: Vec<(usize, &'a SectionProps)>) -> Self { let heads = Linked::new(§ions, |s| s.headers); let feet = Linked::new(§ions, |s| s.footers); HfLinks { sections, heads, feet } } /// The text width of the section that defines header (or, `footer`, footer) `id`, when /// section `si` inherits it rather than naming it (`None` when it names it). Measured in /// Word: an inherited part's percentage-width tables are sized by that section, though it's /// laid out on `si`'s pages. pub(crate) fn defining_width(&self, footer: bool, si: usize, id: Option) -> Option { let id = id?; work::steps(1); let linked = if footer { &self.feet } else { &self.heads }; let own = self.sections.get(si)?.1; if linked.own.get(si).is_some_and(|h| [h.default, h.first, h.even].contains(&Some(id))) { return None; } // The last section before `si` naming it. let namers = linked.namers.get(&id)?; let k = *namers.get(namers.partition_point(|&k| k < si).checked_sub(1)?)?; self.sections.get(k).map(|(_, s)| s.text_width()).filter(|w| (w - own.text_width()).abs() > 0.01) } } /// The headers (or footers) of every section: those it names, and with those it leaves out /// inherited from the sections before it. pub(crate) struct Linked { own: Vec, linked: Vec, /// The sections naming each part, in order. namers: HashMap>, } impl Linked { fn new(sections: &[(usize, &SectionProps)], get: fn(&SectionProps) -> HeaderSet) -> Self { let own: Vec = sections.iter().map(|(_, s)| get(s)).collect(); let mut linked = Vec::with_capacity(own.len()); let mut namers: HashMap> = HashMap::new(); let mut prev = HeaderSet::default(); for (si, o) in own.iter().enumerate() { work::steps(1); prev = HeaderSet { default: o.default.or(prev.default), first: o.first.or(prev.first), even: o.even.or(prev.even) }; linked.push(prev); for id in [o.default, o.first, o.even].into_iter().flatten() { let v = namers.entry(id).or_default(); if v.last() != Some(&si) { v.push(si); } } } Linked { own, linked, namers } } /// Section `si`'s headers (or footers), those it leaves out inherited. pub(crate) fn set(&self, si: usize) -> HeaderSet { self.linked.get(si).copied().unwrap_or_default() } } /// Header rows are re-placed on every page a table spills onto, so a table is O(header items × /// pages) placed items — and an exact-height header row keeps all its overflowing text without /// growing, so a few KB of document could otherwise fill memory. Once this many items (a row /// counts at least one) have been repeated across a table, later pages carry no header rows; /// pagination still finishes. Far above any real table (three ordinary header rows over some /// 5 000 pages). pub(crate) const MAX_HEADER_ITEMS: usize = 500_000; /// Header tables can displace a non-overlapping body table. Page-positioned drawings keep /// their own wrapping rules, so only table areas are retained from the header layout. pub(crate) fn header_table_areas(ctx: &mut Ctx, pb: &PageBuilder) -> Vec { if pb.web || ctx.opts.view == ViewMode::Read { return Vec::new(); } let Some(page) = pb.pages.last() else { return Vec::new() }; let first = pb.pages.iter().rev().nth(1).is_none_or(|prev| prev.section != page.section); let key = (page.section, page.number, first); if let Some(areas) = ctx.header_tables.get(&key) { return areas.clone(); } let hf = Arc::clone(&ctx.hf); let Some((_, sect)) = hf.sections.get(page.section) else { return Vec::new() }; if sect.text_direction.is_turned() { return Vec::new(); } let set = hf.heads.set(page.section); let id = if sect.title_page && first { set.first } else if ctx.doc.settings.even_odd_headers && page.number.is_multiple_of(2) { set.even.or(set.default) } else { set.default }; let Some(id) = id else { return Vec::new() }; let Some(part) = ctx.doc.parts.get(&id) else { return Vec::new() }; let blocks = part.blocks.clone(); let x = sect.margin_left + sect.gutter; let frame = PageFrame { sect, origin: (x, sect.header), number: page.number, mirror: ctx.mirror }; let base = hf.defining_width(false, page.section, Some(id)); let counters = std::mem::take(&mut ctx.counters); let boxes = std::mem::take(&mut ctx.boxes); let chains = std::mem::take(&mut ctx.chains); let fields = ctx.fields.clone(); let grid = std::mem::take(&mut ctx.grid); let track = std::mem::replace(&mut ctx.track_totals, false); ctx.fields.page = page.number; let mut areas = again(ctx, 0, |ctx| { with_pct_base(ctx, base, |ctx| layout_box(ctx, BoxInput::other(StoryRef::Part(id), &blocks, sect.text_width(), 0, Some(frame)))).table_areas }); ctx.counters = counters; ctx.boxes = boxes; ctx.chains = chains; ctx.fields = fields; ctx.grid = grid; ctx.track_totals = track; for r in &mut areas { r.x += x; r.y += sect.header; } ctx.header_tables.insert(key, areas.clone()); areas } /// Whether a header, a footer or a text box shows a `PAGEREF` field, which they resolve from the /// paginated body (see [`bookmark_pages`]). pub(crate) fn headers_footers_refer_to_pages(doc: &Document) -> bool { use wordcraft_doc::PartKind; doc.parts.iter().filter(|(_, part)| matches!(part.kind, PartKind::Header | PartKind::Footer | PartKind::TextBox)).any(|(id, _)| { let story = StoryRef::Part(*id); doc.para_paths(story) .iter() .filter_map(|path| doc.para(story, path)) .any(|p| p.objects.iter().any(|o| matches!(o, InlineObject::Field { instr, .. } if fields::field_name(instr) == "PAGEREF"))) }) } /// Where each bookmark in the body starts: the number and number format of the page it is placed /// on (with two of the same name, the first in the document). pub(crate) fn bookmark_pages(doc: &Document, pages: &[Page], sections: &[(usize, &SectionProps)]) -> fields::BookmarkPages { // Each paragraph's bookmarks: name, offset, and its line in the paragraph layout last looked // up (the pieces of a paragraph usually share one). let mut wanted: HashMap)>> = HashMap::new(); let mut seen = HashSet::new(); for path in doc.para_paths(StoryRef::Body) { let Some(p) = doc.para(StoryRef::Body, &path) else { continue }; if !p.objects.iter().any(|o| matches!(o, InlineObject::BookmarkStart { .. })) { continue; } for (off, o) in p.object_offsets().into_iter().zip(&p.objects) { if let InlineObject::BookmarkStart { name } = o && seen.insert(name.as_str()) { wanted.entry(path.clone()).or_default().push((name.clone(), off, None)); } } } let mut out = fields::BookmarkPages::new(); for page in pages { if wanted.is_empty() { break; } let format = sections.get(page.section).map(|(_, s)| s.page_num_format).unwrap_or_default(); for it in &page.items { let Placed::Lines { story: StoryRef::Body, path, para, l0, l1, .. } = it else { continue }; let Some(marks) = wanted.get_mut(path) else { continue }; let layout = Arc::as_ptr(para) as usize; marks.retain_mut(|(name, off, line)| { let li = match *line { Some((k, li)) if k == layout => li, _ => { let li = para.line_of(*off); *line = Some((layout, li)); li } }; if !(*l0..*l1).contains(&li) { return true; } out.insert(std::mem::take(name), (page.number, format)); false }); if marks.is_empty() { wanted.remove(path); } } } out } /// A key for caches standing for `marks` (0 for none). pub(crate) fn marks_key(marks: &fields::BookmarkPages) -> u64 { if marks.is_empty() { return 0; } let mut v: Vec<_> = marks.iter().collect(); v.sort_by(|a, b| a.0.cmp(b.0)); hash_of(&v) } pub(crate) fn headers_footers(ctx: &mut Ctx, pages: &mut [Page], sections: &[(usize, &SectionProps)], totals: &Totals) { let total = pages.len() as u32; let hf = Arc::clone(&ctx.hf); let even_odd = ctx.doc.settings.even_odd_headers; let styles = styleref::headers_use_styleref(ctx.doc).then(|| ctx.cache.style_index(ctx.doc)); let mut first_of_section: HashSet = HashSet::new(); let mut seen = HashSet::new(); for (i, p) in pages.iter().enumerate() { if seen.insert(p.section) { first_of_section.insert(i); } } for (i, page) in pages.iter_mut().enumerate() { let Some((_, sect)) = sections.get(page.section) else { continue }; // Inherit header/footer references from earlier sections (Word's "link to previous"). let pick = |linked: &Linked| -> Option { let set = linked.set(page.section); if sect.title_page && first_of_section.contains(&i) { return set.first; } if even_odd && page.number % 2 == 0 { return set.even.or(set.default); } set.default }; let hid = pick(&hf.heads); let fid = pick(&hf.feet); page.hf_slot = if sect.title_page && first_of_section.contains(&i) { HfSlot::First } else if even_odd && page.number % 2 == 0 { HfSlot::Even } else { HfSlot::Default }; ctx.fields.page = page.number; ctx.fields.page_format = sect.page_num_format; ctx.fields.pages = total; ctx.fields.section_pages = totals.section(page.section); ctx.fields.section = page.section as u32 + 1; // STYLEREF: the body paragraphs on this page. ctx.fields.styleref = styles.as_ref().map(|ix| { let paths = page.items.iter().filter_map(|it| match it { Placed::Lines { story: StoryRef::Body, path, .. } => Some(path), _ => None, }); (ix.clone(), ix.page_at(paths, page.first_block)) }); let w = sect.text_width(); let x = sect.margin_left + sect.gutter; if let Some(id) = hid && let Some(part) = ctx.doc.parts.get(&id) { let blocks = part.blocks.clone(); let frame = PageFrame { sect, origin: (x, sect.header), number: page.number, mirror: ctx.mirror }; let base = hf.defining_width(false, page.section, hid); let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, |ctx| { with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, Some(frame))).items) }); for it in &mut items { it.translate(x, sect.header); } page.header = behind.into_iter().chain(items).chain(front).collect(); page.header_story = Some(id); } if let Some(id) = fid && let Some(part) = ctx.doc.parts.get(&id) { let blocks = part.blocks.clone(); let base = hf.defining_width(true, page.section, fid); // The footer's height fixes where it sits, which page-relative floats need. let (h, ..) = with_pin(ctx, id, sect, page.number, |ctx| { with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, None)).height) }); let frame = PageFrame { sect, origin: (x, sect.page_h - sect.footer - h), number: page.number, mirror: ctx.mirror }; let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, |ctx| { with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, Some(frame))).items) }); for it in &mut items { it.translate(x, sect.page_h - sect.footer - h); } page.footer = behind.into_iter().chain(items).chain(front).collect(); page.footer_story = Some(id); } } ctx.fields.styleref = None; } /// Mirrored margins: even pages have the inside margin (and gutter) on the right, so their /// header, text, notes and footer move to start at the outside margin. Page borders, measured /// from the page edge, stay. pub(crate) fn mirror_even_pages(pages: &mut [Page], sections: &[(usize, &SectionProps)]) { for page in pages.iter_mut().filter(|p| p.number % 2 == 0) { let Some((_, s)) = sections.get(page.section) else { continue }; let dx = s.margin_right - s.margin_left - s.gutter; if dx.abs() < 0.001 || !dx.is_finite() { continue; } let borders = page_border_rules(s).len(); for it in page.items.iter_mut().skip(borders).chain(page.header.iter_mut()).chain(page.footer.iter_mut()) { it.translate(dx, 0.0); } page.body.x += dx; } }