use super::header_footer::HF_FIXED_SIDE_MARGIN; use super::{ Anchor, CellRef, Json, MAX_COLS, MAX_PAGES, MAX_ROWS, Orientation, PX_TO_PT, PageOrder, PrintSettings, RangeRef, Result, Sheet, Visibility, Workbook, bad, bool_param, split_sheet, str_param, }; /// Cells in a page block above which only stored cells are visited (not every grid position). pub(super) const DENSE_BLOCK: u64 = 60_000; // ---------------------------------------------------------------- paper /// Paper size in points (portrait) for a paper name; unknown names fall back to Letter. pub fn paper_size(name: &str) -> (f32, f32) { known_paper(name).unwrap_or((612.0, 792.0)) } /// Paper sizes in points, portrait. `B4`/`B5`/`B6` are the **JIS** sizes Japanese Excel and its /// e-mail attachments use (257×364, 182×257 and 128×182 mm); the ISO B series stays reachable as /// `B4 (ISO)`/`B5 (ISO)`/`B6 (ISO)`. The Japanese postcards and envelopes are the JIS ones too. pub(super) fn known_paper(name: &str) -> Option<(f32, f32)> { // Spaces, hyphens, underscores and parentheses don't matter: `B5 (JIS)`, `b5-jis` and `b5` // are one name. let n = name.trim().to_ascii_lowercase().replace([' ', '-', '_', '(', ')'], ""); Some(match n.as_str() { "letter" | "usletter" => (612.0, 792.0), "legal" | "uslegal" => (612.0, 1008.0), "a4" => (595.28, 841.89), "a3" => (841.89, 1190.55), "a5" => (419.53, 595.28), "a6" => (297.64, 419.53), "tabloid" | "11x17" => (792.0, 1224.0), "ledger" => (1224.0, 792.0), "executive" => (522.0, 756.0), "b4" | "b4jis" => (728.50, 1031.81), "b5" | "b5jis" => (515.91, 728.50), "b6" | "b6jis" => (362.83, 515.91), "b4iso" => (708.66, 1000.63), "b5iso" => (498.90, 708.66), "b6iso" => (354.33, 498.90), "japanesepostcard" | "postcard" => (283.46, 419.53), "japanesedoublepostcard" | "doublepostcard" => (419.53, 566.93), "japaneseenvelope#3" | "chou#3" => (340.16, 666.14), // 長形4号 is 90×205 mm; 90×225 mm is 長形40号, a different envelope, so it stays out. "japaneseenvelope#4" | "chou#4" => (255.12, 581.10), "japaneseenvelopekaku#2" | "kaku#2" => (680.31, 941.10), "japaneseenvelopekaku#3" | "kaku#3" => (612.28, 785.20), _ => return None, }) } // ---------------------------------------------------------------- layout /// The pagination of one sheet. #[derive(Clone, Debug)] pub struct SheetLayout { pub sheet: usize, pub settings: PrintSettings, pub page_w: f32, pub page_h: f32, /// Left, right, top, bottom, header, footer margins in points. pub margins: [f32; 6], pub scale: f32, pub area: RangeRef, pub row_bands: Vec<(u32, u32)>, pub col_bands: Vec<(u32, u32)>, /// Pages in print order: (row band, column band). pub pages: Vec<(usize, usize)>, pub title_rows: Option<(u32, u32)>, pub title_cols: Option<(u32, u32)>, /// Heading sizes in sheet pixels (0 when headings are off). pub heading_w: f32, pub heading_h: f32, /// Manual page breaks apply (they don't when the sheet is fitted to pages). pub manual_breaks: bool, } impl SheetLayout { pub(super) fn k(&self) -> f32 { PX_TO_PT * self.scale } pub(crate) fn avail(&self) -> (f32, f32) { let [l, r, t, b, ..] = self.margins; ((self.page_w - l - r).max(36.0), (self.page_h - t - b).max(36.0)) } /// Left and right margins, in points, that header and footer sections line up with: the /// page's side margins with Align with Page Margins on, otherwise 0.7 inch on each side, at /// most a quarter of the page width. pub(crate) fn hf_sides(&self) -> (f32, f32) { if self.settings.hf_align_with_margins { (self.margins[0], self.margins[1]) } else { let side = HF_FIXED_SIDE_MARGIN.min(self.page_w / 4.0); (side, side) } } /// Scale of header and footer text: the print scale with Scale with Document on, otherwise 1. pub(crate) fn hf_text_scale(&self) -> f32 { if self.settings.hf_scale_with_doc { self.scale } else { 1.0 } } } /// Options shared by the print commands. #[derive(Clone, Debug, Default)] pub struct PrintOptions { pub all_sheets: bool, pub range: Option, pub fit_to_page: Option, } impl PrintOptions { pub(super) fn from_params(p: &Json, cmd: &str) -> Result { let all_sheets = match str_param(p, "sheets") { None | Some("active") => false, Some("all") | Some("workbook") => true, Some(other) => return Err(bad(cmd, format!("`sheets` must be active or all, not `{other}`"))), }; let range = match str_param(p, "range").map(str::trim).filter(|r| !r.is_empty()) { Some(r) => Some(RangeRef::parse(split_sheet(r).1).ok_or_else(|| bad(cmd, format!("not a range: `{r}`")))?), None => None, }; Ok(PrintOptions { all_sheets, range, fit_to_page: bool_param(p, "fitToPage") }) } } /// The range a sheet prints: its print area, else the used range grown to cover drawing /// objects. `None` when there is nothing to print. pub fn print_range(sh: &Sheet) -> Option { if let Some(a) = sh.print.print_area { return Some(trim_full(sh, a)); } let mut r = sh.used_range(); let mut grow = |a: &Anchor| { let x = sh.col_left(a.cell.col) + a.dx as f64; let y = sh.row_top(a.cell.row) + a.dy as f64; let end = CellRef::new(sh.row_at(y + a.height.max(0.0) as f64), sh.col_at(x + a.width.max(0.0) as f64)); let ar = RangeRef::new(a.cell, end); r = Some(r.map_or(ar, |u| u.union(&ar))); }; for c in &sh.charts { grow(&c.anchor); } for i in &sh.images { grow(&i.anchor); } for s in &sh.shapes { grow(&s.anchor); } // Printing starts at A1 like Excel when the data starts nearby? Excel prints from the first // used cell; keep the used range as is. r } /// Whole rows/columns are trimmed to the used range. fn trim_full(sh: &Sheet, r: RangeRef) -> RangeRef { if !(r.is_full_cols() || r.is_full_rows()) { return r; } match sh.used_range() { Some(u) => r.intersection(&RangeRef::new(CellRef::new(0, 0), u.end)).unwrap_or(RangeRef::cell(r.start)), None => RangeRef::cell(r.start), } } /// Splits `start..=end` into bands whose sizes fit `avail(band_start)`; `breaks` force a new /// band before a line. Stops after `cap` bands. pub(super) fn bands(start: u32, end: u32, size: impl Fn(u32) -> f32, avail: impl Fn(u32) -> f32, breaks: &[u32], cap: usize) -> Vec<(u32, u32)> { let mut out = Vec::new(); let mut cur = start; let mut used = 0.0f32; let mut room = avail(start); let mut line = start; loop { let sz = size(line); let manual = line > cur && breaks.binary_search(&line).is_ok(); if line > cur && (manual || used + sz > room + 0.01) { out.push((cur, line - 1)); if out.len() >= cap { return out; } cur = line; used = 0.0; room = avail(line); } used += sz; if line >= end { break; } line += 1; } out.push((cur, end)); out } fn title_span(t: Option<(u32, u32)>, max: u32) -> Option<(u32, u32)> { let (a, b) = t?; let (a, b) = (a.min(b), a.max(b)); if a >= max { return None; } // Repeated titles are capped (Excel needs them to fit on a page anyway). Some((a, b.min(max - 1).min(a.saturating_add(499)))) } /// Paginates one sheet. pub fn layout_sheet(wb: &Workbook, si: usize, opts: &PrintOptions, range_override: Option) -> Option { let sh = wb.sheet(si)?; let st = sh.print.clone(); let (mut pw, mut ph) = paper_size(&st.paper); if st.orientation == Orientation::Landscape { std::mem::swap(&mut pw, &mut ph); } let inch = |i: usize| st.margins.get(i).copied().filter(|v| v.is_finite()).unwrap_or(0.5).clamp(0.0, 20.0) * 72.0; let mut margins = [inch(0), inch(1), inch(2), inch(3), inch(4), inch(5)]; // Keep at least an inch of printable space. for (a, b, total) in [(0usize, 1usize, pw), (2, 3, ph)] { let sum = margins[a] + margins[b]; if total - sum < 72.0 { let f = ((total - 72.0).max(0.0)) / sum.max(1.0); margins[a] *= f; margins[b] *= f; } } let area = match range_override { Some(r) => trim_full(sh, r), None => print_range(sh).unwrap_or(RangeRef::cell(CellRef::new(0, 0))), }; let title_rows = title_span(st.title_rows, MAX_ROWS); let title_cols = title_span(st.title_cols, MAX_COLS); let digits = format!("{}", area.end.row + 1).len() as f32; let (heading_w, heading_h) = if st.headings { (10.0 + 7.0 * digits, sh.default_row_height.clamp(10.0, 60.0)) } else { (0.0, 0.0) }; let fit = match opts.fit_to_page { Some(true) => Some((1u16, 1u16)), Some(false) => None, None => st.fit_to.filter(|(w, h)| *w > 0 || *h > 0), }; let mut lay = SheetLayout { sheet: si, settings: st.clone(), page_w: pw, page_h: ph, margins, scale: (st.scale.clamp(10, 400) as f32) / 100.0, area, row_bands: vec![], col_bands: vec![], pages: vec![], title_rows, title_cols, heading_w, heading_h, manual_breaks: fit.is_none(), }; let paginate = |lay: &mut SheetLayout, manual: bool| { lay.manual_breaks = manual; (lay.row_bands, lay.col_bands) = split_bands(sh, lay, MAX_PAGES); lay.pages = page_sequence(lay.row_bands.len(), lay.col_bands.len(), lay.settings.page_order, MAX_PAGES); }; match fit { Some((fw, fh)) => { let total_w: f32 = (area.start.col..=area.end.col).map(|c| sh.col_width(c)).sum::() + heading_w; let total_h: f32 = if area.height() > 200_000 { f32::INFINITY } else { (area.start.row..=area.end.row).map(|r| sh.row_height(r)).sum::() + heading_h }; let (aw, ah) = lay.avail(); let sw = if fw > 0 { aw * fw as f32 / (total_w * PX_TO_PT).max(1.0) } else { f32::INFINITY }; let sh_ = if fh > 0 { ah * fh as f32 / (total_h * PX_TO_PT).max(1.0) } else { f32::INFINITY }; lay.scale = sw.min(sh_).min(1.0).clamp(0.1, 1.0); for _ in 0..40 { paginate(&mut lay, false); let ok_w = fw == 0 || lay.col_bands.len() <= fw as usize; let ok_h = fh == 0 || lay.row_bands.len() <= fh as usize; if (ok_w && ok_h) || lay.scale <= 0.1 { break; } lay.scale = (lay.scale * 0.97).max(0.1); } } None => paginate(&mut lay, true), } Some(lay) } /// The pages of a sheet `rows` pages tall and `cols` pages wide as (row band, column band), in /// print order, at most `cap` of them: down each column of pages, then over to the next (Excel's /// default), or over each row of pages, then down to the next. pub(crate) fn page_sequence(rows: usize, cols: usize, order: PageOrder, cap: usize) -> Vec<(usize, usize)> { let (outer, inner) = match order { PageOrder::DownThenOver => (cols, rows), PageOrder::OverThenDown => (rows, cols), }; (0..outer) .flat_map(|o| { (0..inner).map(move |i| match order { PageOrder::DownThenOver => (i, o), PageOrder::OverThenDown => (o, i), }) }) .take(cap) .collect() } /// Inclusive-start/end index spans of the pages along one axis. pub(crate) type Bands = Vec<(u32, u32)>; /// Splits a layout's area into row and column bands that fit its pages (at most `cap` of each), /// honouring manual breaks (when `lay.manual_breaks`), print headings and repeated titles. The /// one pagination rule shared by printing, PDF export, Page Break Preview and Page Layout view. pub(crate) fn split_bands(sh: &Sheet, lay: &SheetLayout, cap: usize) -> (Bands, Bands) { let k = lay.k(); let (aw, ah) = lay.avail(); let (aw, ah) = (aw / k, ah / k); let st = &lay.settings; let mut rb: Vec = if lay.manual_breaks { st.row_breaks.clone() } else { vec![] }; let mut cb: Vec = if lay.manual_breaks { st.col_breaks.clone() } else { vec![] }; rb.sort_unstable(); cb.sort_unstable(); let (title_rows, title_cols) = (lay.title_rows, lay.title_cols); let title_h = |start: u32| -> f32 { match title_rows { Some((a, b)) if a < start => (a..=b.min(start - 1)).map(|r| sh.row_height(r)).sum(), _ => 0.0, } }; let title_w = |start: u32| -> f32 { match title_cols { Some((a, b)) if a < start => (a..=b.min(start - 1)).map(|c| sh.col_width(c)).sum(), _ => 0.0, } }; let area = lay.area; let rows = bands(area.start.row, area.end.row, |r| sh.row_height(r), |start| (ah - lay.heading_h - title_h(start)).max(1.0), &rb, cap); let cols = bands(area.start.col, area.end.col, |c| sh.col_width(c), |start| (aw - lay.heading_w - title_w(start)).max(1.0), &cb, cap); (rows, cols) } /// Lays out every sheet a print command covers. pub fn layouts(wb: &Workbook, opts: &PrintOptions) -> Vec { let mut out = Vec::new(); let mut pages = 0usize; let sheets: Vec = if opts.all_sheets { (0..wb.sheets.len()).filter(|i| wb.sheet(*i).is_some_and(|s| s.visibility == Visibility::Visible)).collect() } else { vec![wb.active_sheet()] }; for si in sheets { if pages >= MAX_PAGES { break; } let ovr = if si == wb.active_sheet() { opts.range } else { None }; if opts.all_sheets && ovr.is_none() && wb.sheet(si).is_some_and(|s| print_range(s).is_none()) { continue; } if let Some(mut l) = layout_sheet(wb, si, opts, ovr) { l.pages.truncate(MAX_PAGES - pages); pages += l.pages.len(); out.push(l); } } if out.is_empty() && opts.all_sheets { out.extend(layout_sheet(wb, wb.active_sheet(), opts, None)); } out } /// Segments of a page along one axis: repeated titles (before the band) then the band. pub(super) fn segments(titles: Option<(u32, u32)>, band: (u32, u32)) -> Vec<(u32, u32)> { let mut v = Vec::new(); if let Some((a, b)) = titles && a < band.0 { v.push((a, b.min(band.0 - 1))); } v.push(band); v } pub(super) fn seg_size(sh: &Sheet, seg: (u32, u32), rows: bool) -> f32 { if rows { (seg.0..=seg.1).map(|r| sh.row_height(r)).sum() } else { (seg.0..=seg.1).map(|c| sh.col_width(c)).sum() } }