Skip to main content

ravenrs_sheet/engine/cmd/print/
layout.rs

1use super::header_footer::HF_FIXED_SIDE_MARGIN;
2use super::{
3    Anchor, CellRef, Json, MAX_COLS, MAX_PAGES, MAX_ROWS, Orientation, PX_TO_PT, PageOrder, PrintSettings, RangeRef, Result, Sheet, Visibility,
4    Workbook, bad, bool_param, split_sheet, str_param,
5};
6
7/// Cells in a page block above which only stored cells are visited (not every grid position).
8pub(super) const DENSE_BLOCK: u64 = 60_000;
9
10// ---------------------------------------------------------------- paper
11
12/// Paper size in points (portrait) for a paper name; unknown names fall back to Letter.
13pub fn paper_size(name: &str) -> (f32, f32) {
14    known_paper(name).unwrap_or((612.0, 792.0))
15}
16
17/// Paper sizes in points, portrait. `B4`/`B5`/`B6` are the **JIS** sizes Japanese Excel and its
18/// e-mail attachments use (257×364, 182×257 and 128×182 mm); the ISO B series stays reachable as
19/// `B4 (ISO)`/`B5 (ISO)`/`B6 (ISO)`. The Japanese postcards and envelopes are the JIS ones too.
20pub(super) fn known_paper(name: &str) -> Option<(f32, f32)> {
21    // Spaces, hyphens, underscores and parentheses don't matter: `B5 (JIS)`, `b5-jis` and `b5`
22    // are one name.
23    let n = name.trim().to_ascii_lowercase().replace([' ', '-', '_', '(', ')'], "");
24    Some(match n.as_str() {
25        "letter" | "usletter" => (612.0, 792.0),
26        "legal" | "uslegal" => (612.0, 1008.0),
27        "a4" => (595.28, 841.89),
28        "a3" => (841.89, 1190.55),
29        "a5" => (419.53, 595.28),
30        "a6" => (297.64, 419.53),
31        "tabloid" | "11x17" => (792.0, 1224.0),
32        "ledger" => (1224.0, 792.0),
33        "executive" => (522.0, 756.0),
34        "b4" | "b4jis" => (728.50, 1031.81),
35        "b5" | "b5jis" => (515.91, 728.50),
36        "b6" | "b6jis" => (362.83, 515.91),
37        "b4iso" => (708.66, 1000.63),
38        "b5iso" => (498.90, 708.66),
39        "b6iso" => (354.33, 498.90),
40        "japanesepostcard" | "postcard" => (283.46, 419.53),
41        "japanesedoublepostcard" | "doublepostcard" => (419.53, 566.93),
42        "japaneseenvelope#3" | "chou#3" => (340.16, 666.14),
43        // 長形4号 is 90×205 mm; 90×225 mm is 長形40号, a different envelope, so it stays out.
44        "japaneseenvelope#4" | "chou#4" => (255.12, 581.10),
45        "japaneseenvelopekaku#2" | "kaku#2" => (680.31, 941.10),
46        "japaneseenvelopekaku#3" | "kaku#3" => (612.28, 785.20),
47        _ => return None,
48    })
49}
50
51// ---------------------------------------------------------------- layout
52
53/// The pagination of one sheet.
54#[derive(Clone, Debug)]
55pub struct SheetLayout {
56    pub sheet: usize,
57    pub settings: PrintSettings,
58    pub page_w: f32,
59    pub page_h: f32,
60    /// Left, right, top, bottom, header, footer margins in points.
61    pub margins: [f32; 6],
62    pub scale: f32,
63    pub area: RangeRef,
64    pub row_bands: Vec<(u32, u32)>,
65    pub col_bands: Vec<(u32, u32)>,
66    /// Pages in print order: (row band, column band).
67    pub pages: Vec<(usize, usize)>,
68    pub title_rows: Option<(u32, u32)>,
69    pub title_cols: Option<(u32, u32)>,
70    /// Heading sizes in sheet pixels (0 when headings are off).
71    pub heading_w: f32,
72    pub heading_h: f32,
73    /// Manual page breaks apply (they don't when the sheet is fitted to pages).
74    pub manual_breaks: bool,
75}
76
77impl SheetLayout {
78    pub(super) fn k(&self) -> f32 {
79        PX_TO_PT * self.scale
80    }
81    pub(crate) fn avail(&self) -> (f32, f32) {
82        let [l, r, t, b, ..] = self.margins;
83        ((self.page_w - l - r).max(36.0), (self.page_h - t - b).max(36.0))
84    }
85    /// Left and right margins, in points, that header and footer sections line up with: the
86    /// page's side margins with Align with Page Margins on, otherwise 0.7 inch on each side, at
87    /// most a quarter of the page width.
88    pub(crate) fn hf_sides(&self) -> (f32, f32) {
89        if self.settings.hf_align_with_margins {
90            (self.margins[0], self.margins[1])
91        } else {
92            let side = HF_FIXED_SIDE_MARGIN.min(self.page_w / 4.0);
93            (side, side)
94        }
95    }
96    /// Scale of header and footer text: the print scale with Scale with Document on, otherwise 1.
97    pub(crate) fn hf_text_scale(&self) -> f32 {
98        if self.settings.hf_scale_with_doc { self.scale } else { 1.0 }
99    }
100}
101
102/// Options shared by the print commands.
103#[derive(Clone, Debug, Default)]
104pub struct PrintOptions {
105    pub all_sheets: bool,
106    pub range: Option<RangeRef>,
107    pub fit_to_page: Option<bool>,
108}
109
110impl PrintOptions {
111    pub(super) fn from_params(p: &Json, cmd: &str) -> Result<PrintOptions> {
112        let all_sheets = match str_param(p, "sheets") {
113            None | Some("active") => false,
114            Some("all") | Some("workbook") => true,
115            Some(other) => return Err(bad(cmd, format!("`sheets` must be active or all, not `{other}`"))),
116        };
117        let range = match str_param(p, "range").map(str::trim).filter(|r| !r.is_empty()) {
118            Some(r) => Some(RangeRef::parse(split_sheet(r).1).ok_or_else(|| bad(cmd, format!("not a range: `{r}`")))?),
119            None => None,
120        };
121        Ok(PrintOptions { all_sheets, range, fit_to_page: bool_param(p, "fitToPage") })
122    }
123}
124
125/// The range a sheet prints: its print area, else the used range grown to cover drawing
126/// objects. `None` when there is nothing to print.
127pub fn print_range(sh: &Sheet) -> Option<RangeRef> {
128    if let Some(a) = sh.print.print_area {
129        return Some(trim_full(sh, a));
130    }
131    let mut r = sh.used_range();
132    let mut grow = |a: &Anchor| {
133        let x = sh.col_left(a.cell.col) + a.dx as f64;
134        let y = sh.row_top(a.cell.row) + a.dy as f64;
135        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));
136        let ar = RangeRef::new(a.cell, end);
137        r = Some(r.map_or(ar, |u| u.union(&ar)));
138    };
139    for c in &sh.charts {
140        grow(&c.anchor);
141    }
142    for i in &sh.images {
143        grow(&i.anchor);
144    }
145    for s in &sh.shapes {
146        grow(&s.anchor);
147    }
148    // Printing starts at A1 like Excel when the data starts nearby? Excel prints from the first
149    // used cell; keep the used range as is.
150    r
151}
152
153/// Whole rows/columns are trimmed to the used range.
154fn trim_full(sh: &Sheet, r: RangeRef) -> RangeRef {
155    if !(r.is_full_cols() || r.is_full_rows()) {
156        return r;
157    }
158    match sh.used_range() {
159        Some(u) => r.intersection(&RangeRef::new(CellRef::new(0, 0), u.end)).unwrap_or(RangeRef::cell(r.start)),
160        None => RangeRef::cell(r.start),
161    }
162}
163
164/// Splits `start..=end` into bands whose sizes fit `avail(band_start)`; `breaks` force a new
165/// band before a line. Stops after `cap` bands.
166pub(super) fn bands(start: u32, end: u32, size: impl Fn(u32) -> f32, avail: impl Fn(u32) -> f32, breaks: &[u32], cap: usize) -> Vec<(u32, u32)> {
167    let mut out = Vec::new();
168    let mut cur = start;
169    let mut used = 0.0f32;
170    let mut room = avail(start);
171    let mut line = start;
172    loop {
173        let sz = size(line);
174        let manual = line > cur && breaks.binary_search(&line).is_ok();
175        if line > cur && (manual || used + sz > room + 0.01) {
176            out.push((cur, line - 1));
177            if out.len() >= cap {
178                return out;
179            }
180            cur = line;
181            used = 0.0;
182            room = avail(line);
183        }
184        used += sz;
185        if line >= end {
186            break;
187        }
188        line += 1;
189    }
190    out.push((cur, end));
191    out
192}
193
194fn title_span(t: Option<(u32, u32)>, max: u32) -> Option<(u32, u32)> {
195    let (a, b) = t?;
196    let (a, b) = (a.min(b), a.max(b));
197    if a >= max {
198        return None;
199    }
200    // Repeated titles are capped (Excel needs them to fit on a page anyway).
201    Some((a, b.min(max - 1).min(a.saturating_add(499))))
202}
203
204/// Paginates one sheet.
205pub fn layout_sheet(wb: &Workbook, si: usize, opts: &PrintOptions, range_override: Option<RangeRef>) -> Option<SheetLayout> {
206    let sh = wb.sheet(si)?;
207    let st = sh.print.clone();
208    let (mut pw, mut ph) = paper_size(&st.paper);
209    if st.orientation == Orientation::Landscape {
210        std::mem::swap(&mut pw, &mut ph);
211    }
212    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;
213    let mut margins = [inch(0), inch(1), inch(2), inch(3), inch(4), inch(5)];
214    // Keep at least an inch of printable space.
215    for (a, b, total) in [(0usize, 1usize, pw), (2, 3, ph)] {
216        let sum = margins[a] + margins[b];
217        if total - sum < 72.0 {
218            let f = ((total - 72.0).max(0.0)) / sum.max(1.0);
219            margins[a] *= f;
220            margins[b] *= f;
221        }
222    }
223    let area = match range_override {
224        Some(r) => trim_full(sh, r),
225        None => print_range(sh).unwrap_or(RangeRef::cell(CellRef::new(0, 0))),
226    };
227    let title_rows = title_span(st.title_rows, MAX_ROWS);
228    let title_cols = title_span(st.title_cols, MAX_COLS);
229    let digits = format!("{}", area.end.row + 1).len() as f32;
230    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) };
231    let fit = match opts.fit_to_page {
232        Some(true) => Some((1u16, 1u16)),
233        Some(false) => None,
234        None => st.fit_to.filter(|(w, h)| *w > 0 || *h > 0),
235    };
236    let mut lay = SheetLayout {
237        sheet: si,
238        settings: st.clone(),
239        page_w: pw,
240        page_h: ph,
241        margins,
242        scale: (st.scale.clamp(10, 400) as f32) / 100.0,
243        area,
244        row_bands: vec![],
245        col_bands: vec![],
246        pages: vec![],
247        title_rows,
248        title_cols,
249        heading_w,
250        heading_h,
251        manual_breaks: fit.is_none(),
252    };
253    let paginate = |lay: &mut SheetLayout, manual: bool| {
254        lay.manual_breaks = manual;
255        (lay.row_bands, lay.col_bands) = split_bands(sh, lay, MAX_PAGES);
256        lay.pages = page_sequence(lay.row_bands.len(), lay.col_bands.len(), lay.settings.page_order, MAX_PAGES);
257    };
258    match fit {
259        Some((fw, fh)) => {
260            let total_w: f32 = (area.start.col..=area.end.col).map(|c| sh.col_width(c)).sum::<f32>() + heading_w;
261            let total_h: f32 = if area.height() > 200_000 {
262                f32::INFINITY
263            } else {
264                (area.start.row..=area.end.row).map(|r| sh.row_height(r)).sum::<f32>() + heading_h
265            };
266            let (aw, ah) = lay.avail();
267            let sw = if fw > 0 { aw * fw as f32 / (total_w * PX_TO_PT).max(1.0) } else { f32::INFINITY };
268            let sh_ = if fh > 0 { ah * fh as f32 / (total_h * PX_TO_PT).max(1.0) } else { f32::INFINITY };
269            lay.scale = sw.min(sh_).min(1.0).clamp(0.1, 1.0);
270            for _ in 0..40 {
271                paginate(&mut lay, false);
272                let ok_w = fw == 0 || lay.col_bands.len() <= fw as usize;
273                let ok_h = fh == 0 || lay.row_bands.len() <= fh as usize;
274                if (ok_w && ok_h) || lay.scale <= 0.1 {
275                    break;
276                }
277                lay.scale = (lay.scale * 0.97).max(0.1);
278            }
279        }
280        None => paginate(&mut lay, true),
281    }
282    Some(lay)
283}
284
285/// The pages of a sheet `rows` pages tall and `cols` pages wide as (row band, column band), in
286/// print order, at most `cap` of them: down each column of pages, then over to the next (Excel's
287/// default), or over each row of pages, then down to the next.
288pub(crate) fn page_sequence(rows: usize, cols: usize, order: PageOrder, cap: usize) -> Vec<(usize, usize)> {
289    let (outer, inner) = match order {
290        PageOrder::DownThenOver => (cols, rows),
291        PageOrder::OverThenDown => (rows, cols),
292    };
293    (0..outer)
294        .flat_map(|o| {
295            (0..inner).map(move |i| match order {
296                PageOrder::DownThenOver => (i, o),
297                PageOrder::OverThenDown => (o, i),
298            })
299        })
300        .take(cap)
301        .collect()
302}
303
304/// Inclusive-start/end index spans of the pages along one axis.
305pub(crate) type Bands = Vec<(u32, u32)>;
306
307/// Splits a layout's area into row and column bands that fit its pages (at most `cap` of each),
308/// honouring manual breaks (when `lay.manual_breaks`), print headings and repeated titles. The
309/// one pagination rule shared by printing, PDF export, Page Break Preview and Page Layout view.
310pub(crate) fn split_bands(sh: &Sheet, lay: &SheetLayout, cap: usize) -> (Bands, Bands) {
311    let k = lay.k();
312    let (aw, ah) = lay.avail();
313    let (aw, ah) = (aw / k, ah / k);
314    let st = &lay.settings;
315    let mut rb: Vec<u32> = if lay.manual_breaks { st.row_breaks.clone() } else { vec![] };
316    let mut cb: Vec<u32> = if lay.manual_breaks { st.col_breaks.clone() } else { vec![] };
317    rb.sort_unstable();
318    cb.sort_unstable();
319    let (title_rows, title_cols) = (lay.title_rows, lay.title_cols);
320    let title_h = |start: u32| -> f32 {
321        match title_rows {
322            Some((a, b)) if a < start => (a..=b.min(start - 1)).map(|r| sh.row_height(r)).sum(),
323            _ => 0.0,
324        }
325    };
326    let title_w = |start: u32| -> f32 {
327        match title_cols {
328            Some((a, b)) if a < start => (a..=b.min(start - 1)).map(|c| sh.col_width(c)).sum(),
329            _ => 0.0,
330        }
331    };
332    let area = lay.area;
333    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);
334    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);
335    (rows, cols)
336}
337
338/// Lays out every sheet a print command covers.
339pub fn layouts(wb: &Workbook, opts: &PrintOptions) -> Vec<SheetLayout> {
340    let mut out = Vec::new();
341    let mut pages = 0usize;
342    let sheets: Vec<usize> = if opts.all_sheets {
343        (0..wb.sheets.len()).filter(|i| wb.sheet(*i).is_some_and(|s| s.visibility == Visibility::Visible)).collect()
344    } else {
345        vec![wb.active_sheet()]
346    };
347    for si in sheets {
348        if pages >= MAX_PAGES {
349            break;
350        }
351        let ovr = if si == wb.active_sheet() { opts.range } else { None };
352        if opts.all_sheets && ovr.is_none() && wb.sheet(si).is_some_and(|s| print_range(s).is_none()) {
353            continue;
354        }
355        if let Some(mut l) = layout_sheet(wb, si, opts, ovr) {
356            l.pages.truncate(MAX_PAGES - pages);
357            pages += l.pages.len();
358            out.push(l);
359        }
360    }
361    if out.is_empty() && opts.all_sheets {
362        out.extend(layout_sheet(wb, wb.active_sheet(), opts, None));
363    }
364    out
365}
366
367/// Segments of a page along one axis: repeated titles (before the band) then the band.
368pub(super) fn segments(titles: Option<(u32, u32)>, band: (u32, u32)) -> Vec<(u32, u32)> {
369    let mut v = Vec::new();
370    if let Some((a, b)) = titles
371        && a < band.0
372    {
373        v.push((a, b.min(band.0 - 1)));
374    }
375    v.push(band);
376    v
377}
378
379pub(super) fn seg_size(sh: &Sheet, seg: (u32, u32), rows: bool) -> f32 {
380    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() }
381}