1use crate::base::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
13#[cfg(test)]
14use crate::model::{Alignment, Image, SheetViewMode};
15use crate::model::{
16 Anchor, BorderLine, BorderStyle, Borders, Color, HAlign, Orientation, PageOrder, PatternType, PrintSettings, ShapeKind, Sheet, Style, StyleId,
17 StyleTable, Underline, VAlign, Visibility, Workbook,
18};
19use crate::pdf::{Font, Page, PdfDoc, Point, Rect, Rgb};
20use serde_json::{Value as Json, json};
21
22use super::{CommandSpec, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, split_sheet, str_param, target_sheet, u32_param};
23
24mod draw;
25mod header_footer;
26mod layout;
27mod objects;
28mod text;
29
30use draw::{Block, PrintedBlock, draw_block, draw_block_links, draw_headings, make_block, printed_target};
31use header_footer::draw_header_footer;
32pub use header_footer::{
33 HF_FIELDS, HfContext, HfSection, codes_to_fields, expand_header_footer, fields_to_codes, hf_date_time, join_header_footer, split_header_footer,
34};
35type PicCache = std::collections::HashMap<(usize, usize), Option<(Vec<u8>, u32, u32)>>;
37
38#[cfg(test)]
39use layout::bands;
40pub(crate) use layout::split_bands;
41pub use layout::{PrintOptions, SheetLayout, layout_sheet, layouts, paper_size, print_range};
42use layout::{known_paper, seg_size, segments};
43use objects::draw_objects;
44#[cfg(test)]
45use text::{center_across_width, wrap_lines};
46
47pub fn specs() -> Vec<CommandSpec> {
48 vec![
49 cmd!(
50 noundo "file.exportPdf",
51 "Export as PDF",
52 ["File", "Export"],
53 None,
54 "{path?, sheets?: active|all, range?: \"A1:F40\" (active sheet), fitToPage?: bool} → {pages, bytes, path} or {pages, bytes, base64} when no path. Honours each sheet's Page Setup.",
55 has_doc,
56 export_pdf
57 ),
58 cmd!(
59 noundo "file.print",
60 "Print…",
61 ["File"],
62 Some("Cmd+P"),
63 "{path?, sheets?: active|all, range?, fitToPage?: bool} → {pages, bytes, base64} — the PDF the UI hands to the system print dialog",
64 has_doc,
65 print
66 ),
67 cmd!(
68 query "file.printPreview",
69 "Print Preview",
70 ["File"],
71 None,
72 "{page? (1-based), sheets?: active|all, range?, fitToPage?} → {pages, pageList: [{page, sheet, range, rows, columns, scale, paper, orientation, pageOrder, width, height}]} in print order",
73 has_doc,
74 print_preview
75 ),
76 cmd!(
77 "pageLayout.pageSetup",
78 "Page Setup…",
79 ["Page Layout", "Page Setup"],
80 None,
81 "{sheet?, orientation?: portrait|landscape, paper?: Letter|Legal|A4|A3|A5|A6|Tabloid|Executive|Ledger|B4|B5|B6|B4 (ISO)|B5 (ISO)|B6 (ISO)|Japanese Postcard|Japanese Double Postcard|Japanese Envelope #3|Japanese Envelope #4|Japanese Envelope Kaku #2|Japanese Envelope Kaku #3 (B4/B5/B6 are the JIS sizes), margins?: {left,right,top,bottom,header,footer} (inches; also accepted as top-level keys), scale?: 10..400, fitWidth?, fitHeight? (pages; 0 = automatic), fitToPage?: bool, centerH?, centerV?, gridlines?, headings?, header?: \"&L&A&RPage &P of &N\", footer?, printArea?: \"A1:F50\" (\"\" clears), titleRows?: \"1:2\", titleCols?: \"A:A\", rowBreaks?: [46, 91], colBreaks?: [\"K\"], pageOrder?: downThenOver|overThenDown} → the sheet's print settings",
82 has_doc,
83 page_setup
84 ),
85 ]
86}
87
88pub(crate) const PX_TO_PT: f32 = 0.75;
90pub const MAX_PAGES: usize = 2000;
92
93pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str) -> (Vec<u8>, usize) {
95 let mut doc = PdfDoc::new();
96 doc.info.title = if wb.props.title.is_empty() { file.to_string() } else { wb.props.title.clone() };
97 doc.info.author = wb.props.author.clone();
98 let total: usize = layouts.iter().map(|l| l.pages.len()).sum();
99 let (date, time) = hf_date_time(wb);
100 let mut page_no = 0usize;
101 let mut pending_links = Vec::new();
102 let mut printed_blocks = Vec::new();
103 let mut pics = PicCache::new();
106 for lay in layouts {
107 let Some(sh) = wb.sheet(lay.sheet) else { continue };
108 let mut cf = if sh.cond_formats.is_empty() { None } else { Some(crate::engine::cf::CfCache::new()) };
109 let k = lay.k();
110 let (aw, ah) = lay.avail();
111 for (pi, (ri, ci)) in lay.pages.iter().enumerate() {
112 page_no += 1;
113 let (Some(rband), Some(cband)) = (lay.row_bands.get(*ri).copied(), lay.col_bands.get(*ci).copied()) else { continue };
114 let rsegs = segments(lay.title_rows, rband);
115 let csegs = segments(lay.title_cols, cband);
116 let content_w = (lay.heading_w + csegs.iter().map(|s| seg_size(sh, *s, false)).sum::<f32>()) * k;
117 let content_h = (lay.heading_h + rsegs.iter().map(|s| seg_size(sh, *s, true)).sum::<f32>()) * k;
118 let ox = lay.margins[0] + if lay.settings.center_h { ((aw - content_w) / 2.0).max(0.0) } else { 0.0 };
119 let oy = lay.margins[2] + if lay.settings.center_v { ((ah - content_h) / 2.0).max(0.0) } else { 0.0 };
120 let page_index = doc.page_count();
121 let pg = doc.add_page(lay.page_w, lay.page_h);
122 let mut y = oy + lay.heading_h * k;
123 let mut body: Option<Block> = None;
124 for (rsi, rs) in rsegs.iter().enumerate() {
125 let mut x = ox + lay.heading_w * k;
126 for (csi, cs) in csegs.iter().enumerate() {
127 let b = make_block(wb, lay.sheet, sh, *rs, *cs, (x, y), lay);
128 draw_block(pg, &b, lay.settings.gridlines, &mut cf);
129 draw_block_links(pg, &b, page_index, &mut pending_links);
130 printed_blocks.push(PrintedBlock { page: page_index, sheet: lay.sheet, rows: *rs, cols: *cs, x, y, k });
131 if lay.settings.headings {
132 draw_headings(pg, &b, rsi == 0, csi == 0, (ox, oy), (lay.heading_w * k, lay.heading_h * k));
133 }
134 x += seg_size(sh, *cs, false) * k;
135 if rsi + 1 == rsegs.len() && csi + 1 == csegs.len() {
136 body = Some(b);
137 }
138 }
139 y += seg_size(sh, *rs, true) * k;
140 }
141 if let Some(b) = &body {
142 draw_objects(pg, wb, lay.sheet, sh, b, &mut pics);
143 }
144 if lay.settings.headings {
145 let fill = [0xF2, 0xF2, 0xF2];
146 pg.fill_rect(ox, oy, lay.heading_w * k, lay.heading_h * k, fill);
147 pg.rect_stroke(ox, oy, lay.heading_w * k, lay.heading_h * k, 0.3, [0xA0, 0xA0, 0xA0]);
148 }
149 let cx = HfContext {
150 page: page_no,
151 pages: total,
152 sheet: sh.name.clone(),
153 file: file.to_string(),
154 path: path.to_string(),
155 date: date.clone(),
156 time: time.clone(),
157 };
158 draw_header_footer(pg, lay, lay.settings.hf_page(pi == 0, page_no), &cx);
159 }
160 }
161 for link in pending_links {
162 if let Some((target_page, x, y)) = printed_target(wb, &printed_blocks, link.sheet, &link.target)
163 && let Some(page) = doc.page_mut(link.page)
164 {
165 let (lx, ly, w, h) = link.rect;
166 page.link_page(Rect::new(lx, ly, w, h), target_page, Point::new(x, y));
167 }
168 }
169 let n = doc.page_count();
170 (doc.finish(), n)
171}
172
173fn build_pdf(s: &Session, p: &Json, cmd: &str) -> Result<(Vec<u8>, usize)> {
176 let opts = PrintOptions::from_params(p, cmd)?;
177 let d = s.doc()?;
178 let lays = layouts(&d.wb, &opts);
179 let path = d.path.clone().unwrap_or_default();
180 Ok(render_pdf(&d.wb, &lays, &d.display_title(), &path))
181}
182
183pub(crate) fn export_pdf(s: &mut Session, p: &Json) -> Result<Json> {
184 let (bytes, pages) = build_pdf(s, p, "file.exportPdf")?;
185 match str_param(p, "path").filter(|x| !x.is_empty()) {
186 Some(path) => {
187 crate::engine::io::write_file(path, &bytes)?;
188 Ok(json!({"pages": pages, "bytes": bytes.len(), "path": path}))
189 }
190 None => Ok(json!({"pages": pages, "bytes": bytes.len(), "base64": crate::engine::io::base64_encode(&bytes)})),
191 }
192}
193
194fn print(s: &mut Session, p: &Json) -> Result<Json> {
195 let (bytes, pages) = build_pdf(s, p, "file.print")?;
196 let mut out = json!({"pages": pages, "bytes": bytes.len(), "base64": crate::engine::io::base64_encode(&bytes)});
197 if let Some(path) = str_param(p, "path").filter(|x| !x.is_empty()) {
198 crate::engine::io::write_file(path, &bytes)?;
199 out["path"] = json!(path);
200 }
201 Ok(out)
202}
203
204fn print_preview(s: &mut Session, p: &Json) -> Result<Json> {
205 let opts = PrintOptions::from_params(p, "file.printPreview")?;
206 let d = s.doc()?;
207 let lays = layouts(&d.wb, &opts);
208 let mut list = Vec::new();
209 let mut n = 0usize;
210 for lay in &lays {
211 let name = d.wb.sheet(lay.sheet).map(|s| s.name.clone()).unwrap_or_default();
212 for (ri, ci) in &lay.pages {
213 n += 1;
214 let (Some(rb), Some(cb)) = (lay.row_bands.get(*ri), lay.col_bands.get(*ci)) else { continue };
215 let r = RangeRef::new(CellRef::new(rb.0, cb.0), CellRef::new(rb.1, cb.1));
216 list.push(json!({
217 "page": n,
218 "sheet": name,
219 "range": r.a1(),
220 "rows": format!("{}:{}", rb.0 + 1, rb.1 + 1),
221 "columns": format!("{}:{}", crate::base::col_to_letters(cb.0), crate::base::col_to_letters(cb.1)),
222 "scale": (lay.scale * 1000.0).round() / 10.0,
223 "paper": lay.settings.paper,
224 "orientation": if lay.settings.orientation == Orientation::Landscape { "landscape" } else { "portrait" },
225 "pageOrder": lay.settings.page_order.id(),
226 "width": lay.page_w,
227 "height": lay.page_h,
228 }));
229 }
230 }
231 if let Some(page) = u32_param(p, "page") {
232 let one =
233 list.get((page as usize).saturating_sub(1)).cloned().ok_or_else(|| bad("file.printPreview", format!("there are only {n} pages")))?;
234 return Ok(json!({"pages": n, "page": one}));
235 }
236 Ok(json!({"pages": n, "pageList": list}))
237}
238
239fn page_setup(s: &mut Session, p: &Json) -> Result<Json> {
240 const CMD: &str = "pageLayout.pageSetup";
241 let si = target_sheet(s, p)?;
242 let orientation = match str_param(p, "orientation") {
243 None => None,
244 Some("portrait") => Some(Orientation::Portrait),
245 Some("landscape") => Some(Orientation::Landscape),
246 Some(o) => return Err(bad(CMD, format!("orientation must be portrait or landscape, not `{o}`"))),
247 };
248 let page_order = match p.get("pageOrder") {
249 None => None,
250 Some(v) => match v.as_str().and_then(PageOrder::parse) {
251 Some(o) => Some(o),
252 None => {
253 let shown = v.as_str().map_or_else(|| v.to_string(), str::to_string);
254 return Err(bad(CMD, format!("pageOrder must be downThenOver or overThenDown, not `{shown}`")));
255 }
256 },
257 };
258 let paper = match str_param(p, "paper") {
259 None => None,
260 Some(name) => {
261 known_paper(name).ok_or_else(|| bad(CMD, format!("unknown paper size `{name}`")))?;
262 Some(name.trim().to_string())
263 }
264 };
265 let margin_src = p.get("margins").filter(|m| m.is_object()).unwrap_or(p);
266 let mut margins: [Option<f32>; 6] = [None; 6];
267 for (i, key) in ["left", "right", "top", "bottom", "header", "footer"].iter().enumerate() {
268 if let Some(v) = f64_param(margin_src, key).or_else(|| f64_param(p, key)) {
269 if !v.is_finite() || v < 0.0 {
270 return Err(bad(CMD, format!("margin `{key}` must be a non-negative number of inches")));
271 }
272 margins[i] = Some(v.min(20.0) as f32);
273 }
274 }
275 let parse_rows = |key: &str| -> Result<Option<Option<(u32, u32)>>> {
276 match p.get(key) {
277 None => Ok(None),
278 Some(Json::Null) => Ok(Some(None)),
279 Some(Json::String(t)) if t.trim().is_empty() => Ok(Some(None)),
280 Some(Json::String(t)) => {
281 let r = RangeRef::parse(split_sheet(t).1).ok_or_else(|| bad(CMD, format!("`{key}` is not a row range: `{t}`")))?;
282 Ok(Some(Some((r.start.row, r.end.row))))
283 }
284 Some(_) => Err(bad(CMD, format!("`{key}` must be text like \"1:2\""))),
285 }
286 };
287 let parse_cols = |key: &str| -> Result<Option<Option<(u32, u32)>>> {
288 match p.get(key) {
289 None => Ok(None),
290 Some(Json::Null) => Ok(Some(None)),
291 Some(Json::String(t)) if t.trim().is_empty() => Ok(Some(None)),
292 Some(Json::String(t)) => {
293 let r = RangeRef::parse(split_sheet(t).1).ok_or_else(|| bad(CMD, format!("`{key}` is not a column range: `{t}`")))?;
294 Ok(Some(Some((r.start.col, r.end.col))))
295 }
296 Some(_) => Err(bad(CMD, format!("`{key}` must be text like \"A:B\""))),
297 }
298 };
299 let title_rows = parse_rows("titleRows")?;
300 let title_cols = parse_cols("titleCols")?;
301 let print_area = match p.get("printArea") {
302 None => None,
303 Some(Json::Null) => Some(None),
304 Some(Json::String(t)) if t.trim().is_empty() => Some(None),
305 Some(Json::String(t)) => Some(Some(RangeRef::parse(split_sheet(t).1).ok_or_else(|| bad(CMD, format!("`printArea` is not a range: `{t}`")))?)),
306 Some(_) => return Err(bad(CMD, "`printArea` must be a range like \"A1:F50\"")),
307 };
308 let row_breaks: Option<Vec<u32>> = p.get("rowBreaks").and_then(Json::as_array).map(|a| {
309 let mut v: Vec<u32> = a
310 .iter()
311 .filter_map(|x| x.as_u64().or_else(|| x.as_str().and_then(|t| t.trim().parse().ok())))
312 .filter(|r| *r >= 2 && *r <= MAX_ROWS as u64)
313 .map(|r| r as u32 - 1)
314 .take(1026)
315 .collect();
316 v.sort_unstable();
317 v.dedup();
318 v
319 });
320 let col_breaks: Option<Vec<u32>> = p.get("colBreaks").and_then(Json::as_array).map(|a| {
321 let mut v: Vec<u32> = a
322 .iter()
323 .filter_map(|x| match x {
324 Json::String(t) => crate::base::letters_to_col(t.trim().trim_start_matches('$')),
325 Json::Number(n) => n.as_u64().filter(|c| *c >= 2 && *c <= MAX_COLS as u64).map(|c| c as u32 - 1),
326 _ => None,
327 })
328 .filter(|c| *c >= 1)
329 .take(1026)
330 .collect();
331 v.sort_unstable();
332 v.dedup();
333 v
334 });
335 let scale = f64_param(p, "scale").filter(|v| v.is_finite()).map(|v| v.clamp(10.0, 400.0).round() as u16);
336 let fit_w = p.get("fitWidth").map(|v| v.as_u64().unwrap_or(0).min(32767) as u16);
337 let fit_h = p.get("fitHeight").map(|v| v.as_u64().unwrap_or(0).min(32767) as u16);
338 let fit_page = bool_param(p, "fitToPage");
339 let header = str_param(p, "header").map(|t| t.chars().take(255).collect::<String>());
340 let footer = str_param(p, "footer").map(|t| t.chars().take(255).collect::<String>());
341 let flags = [("centerH", 0usize), ("centerV", 1), ("gridlines", 2), ("headings", 3)].map(|(k, i)| (i, bool_param(p, k)));
342 edit(s, |cx| {
343 let pr = &mut cx.sheet_mut(si)?.print;
344 if let Some(o) = orientation {
345 pr.orientation = o;
346 }
347 if let Some(pp) = &paper {
348 pr.paper = pp.clone();
349 }
350 for (i, m) in margins.iter().enumerate() {
351 if let (Some(v), Some(slot)) = (m, pr.margins.get_mut(i)) {
352 *slot = *v;
353 }
354 }
355 if let Some(sc) = scale {
356 pr.scale = sc;
357 pr.fit_to = None;
358 }
359 if fit_w.is_some() || fit_h.is_some() {
360 let (cw, ch) = pr.fit_to.unwrap_or((1, 1));
361 pr.fit_to = Some((fit_w.unwrap_or(cw), fit_h.unwrap_or(ch)));
362 }
363 match fit_page {
364 Some(true) => pr.fit_to = Some((1, 1)),
365 Some(false) => pr.fit_to = None,
366 None => {}
367 }
368 for (i, v) in flags {
369 if let Some(v) = v {
370 match i {
371 0 => pr.center_h = v,
372 1 => pr.center_v = v,
373 2 => pr.gridlines = v,
374 _ => pr.headings = v,
375 }
376 }
377 }
378 if let Some(h) = &header {
379 pr.header = h.clone();
380 }
381 if let Some(f) = &footer {
382 pr.footer = f.clone();
383 }
384 if let Some(a) = print_area {
385 pr.print_area = a;
386 }
387 if let Some(t) = title_rows {
388 pr.title_rows = t;
389 }
390 if let Some(t) = title_cols {
391 pr.title_cols = t;
392 }
393 if let Some(b) = &row_breaks {
394 pr.row_breaks = b.clone();
395 }
396 if let Some(b) = &col_breaks {
397 pr.col_breaks = b.clone();
398 }
399 if let Some(o) = page_order {
400 pr.page_order = o;
401 }
402 Ok(())
403 })?;
404 let pr = s.doc()?.wb.sheet(si).map(|sh| sh.print.clone()).unwrap_or_default();
405 Ok(serde_json::to_value(&pr).unwrap_or(Json::Null))
406}
407
408#[cfg(test)]
409mod tests;