Files
gridcraft/crates/engine/src/cmd/print/layout.rs
T

371 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::header_footer::HF_FIXED_SIDE_MARGIN;
use super::{
Anchor, CellRef, Json, MAX_COLS, MAX_PAGES, MAX_ROWS, Orientation, PX_TO_PT, 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<RangeRef>,
pub fit_to_page: Option<bool>,
}
impl PrintOptions {
pub(super) fn from_params(p: &Json, cmd: &str) -> Result<PrintOptions> {
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<RangeRef> {
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<RangeRef>) -> Option<SheetLayout> {
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.clear();
'outer: for ci in 0..lay.col_bands.len() {
for ri in 0..lay.row_bands.len() {
if lay.pages.len() >= MAX_PAGES {
break 'outer;
}
lay.pages.push((ri, ci));
}
}
};
match fit {
Some((fw, fh)) => {
let total_w: f32 = (area.start.col..=area.end.col).map(|c| sh.col_width(c)).sum::<f32>() + 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::<f32>() + 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)
}
/// 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<u32> = if lay.manual_breaks { st.row_breaks.clone() } else { vec![] };
let mut cb: Vec<u32> = 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<SheetLayout> {
let mut out = Vec::new();
let mut pages = 0usize;
let sheets: Vec<usize> = 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() }
}