mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge PR #2131: Print: JIS B4/B5 and Japanese paper sizes (はがき・封筒・B6)
ROADMAP.md: one status line naming both changes; both feature-table and progress-log rows, newest first.
This commit is contained in:
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Page Setup…",
|
||||
["Page Layout", "Page Setup"],
|
||||
None,
|
||||
"{sheet?, orientation?: portrait|landscape, paper?: Letter|Legal|A4|A3|A5|Tabloid|Executive|Ledger|B5, 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\"]} → the sheet's print settings",
|
||||
"{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\"]} → the sheet's print settings",
|
||||
has_doc,
|
||||
page_setup
|
||||
),
|
||||
@@ -71,19 +71,36 @@ 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.
|
||||
fn known_paper(name: &str) -> Option<(f32, f32)> {
|
||||
let n = name.trim().to_ascii_lowercase().replace([' ', '-', '_'], "");
|
||||
// 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),
|
||||
"b5" => (498.9, 708.66),
|
||||
"b4" => (708.66, 1000.63),
|
||||
"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,
|
||||
})
|
||||
}
|
||||
@@ -2061,6 +2078,81 @@ mod tests {
|
||||
assert!(s.doc().unwrap().wb.active().unwrap().print.print_area.is_some());
|
||||
}
|
||||
|
||||
/// The Japanese paper sizes are the JIS ones: `B4`/`B5`/`B6` changed meaning from ISO, and the
|
||||
/// ISO B series stays reachable by name. The postcards and envelopes are the JIS sizes too.
|
||||
#[test]
|
||||
fn japanese_paper_sizes_are_jis() {
|
||||
for (name, w, h) in [
|
||||
("A4", 595.28, 841.89), // unchanged
|
||||
("B4", 728.50, 1031.81),
|
||||
("B5", 515.91, 728.50),
|
||||
("B6", 362.83, 515.91),
|
||||
("B4 (JIS)", 728.50, 1031.81),
|
||||
("b5-jis", 515.91, 728.50),
|
||||
("B4 (ISO)", 708.66, 1000.63),
|
||||
("B5 (ISO)", 498.90, 708.66),
|
||||
("B6 (ISO)", 354.33, 498.90),
|
||||
("A6", 297.64, 419.53),
|
||||
("Japanese Postcard", 283.46, 419.53),
|
||||
("japanese double postcard", 419.53, 566.93),
|
||||
("Japanese Envelope #3", 340.16, 666.14),
|
||||
("chou #4", 255.12, 581.10),
|
||||
("Japanese Envelope Kaku #2", 680.31, 941.10),
|
||||
("kaku #3", 612.28, 785.20),
|
||||
] {
|
||||
let got = paper_size(name);
|
||||
assert!((got.0 - w).abs() < 0.01 && (got.1 - h).abs() < 0.01, "{name}: {got:?}");
|
||||
}
|
||||
// 長形4号 is 90×205 mm (90×225 mm is 長形40号), so its height is 205 mm = 581.10 pt — not the
|
||||
// 225 mm it once had.
|
||||
let chou4 = paper_size("Japanese Envelope #4");
|
||||
assert_eq!((chou4.0, chou4.1), (255.12, 581.10));
|
||||
assert_eq!(chou4, paper_size("chou #4"));
|
||||
// Unknown names keep the old Letter fallback.
|
||||
assert_eq!(paper_size("Napkin"), (612.0, 792.0));
|
||||
}
|
||||
|
||||
/// Page Setup's paper reaches the PDF page: the MediaBox is the paper's, A4 is unchanged, B5 is
|
||||
/// JIS B5, and the Japanese postcard prints at 283.46×419.53 pt. Landscape swaps the sides and
|
||||
/// fit-to-page keeps the paper size.
|
||||
#[test]
|
||||
fn page_setup_paper_sizes_reach_the_pdf_page() {
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["x"]]})).unwrap();
|
||||
let close = |v: &Json, want: f64| (v.as_f64().unwrap() - want).abs() < 0.01;
|
||||
for (paper, w, h) in [
|
||||
("A4", 595.28f64, 841.89f64),
|
||||
("B5", 515.91, 728.50),
|
||||
("Japanese Postcard", 283.46, 419.53),
|
||||
("Japanese Double Postcard", 419.53, 566.93),
|
||||
("Japanese Envelope Kaku #2", 680.31, 941.10),
|
||||
] {
|
||||
s.execute("pageLayout.pageSetup", json!({"paper": paper, "orientation": "portrait"})).unwrap();
|
||||
let pv = s.execute("file.printPreview", json!({})).unwrap();
|
||||
let page = &pv["pageList"][0];
|
||||
assert_eq!(page["paper"], paper);
|
||||
assert!(close(&page["width"], w) && close(&page["height"], h), "{paper}: {page}");
|
||||
let (pdf, pages) = pdf_of(&mut s, json!({}));
|
||||
assert_eq!(pages, 1);
|
||||
let want = format!("/MediaBox [0 0 {w} {h}]");
|
||||
assert!(contains(&pdf, &want), "{paper}: wanted {want}");
|
||||
}
|
||||
// Landscape swaps the sides, and fit-to-page does not change the paper.
|
||||
s.execute("pageLayout.pageSetup", json!({"paper": "B5", "orientation": "landscape"})).unwrap();
|
||||
let (pdf, pages) = pdf_of(&mut s, json!({}));
|
||||
assert_eq!(pages, 1);
|
||||
assert!(contains(&pdf, "/MediaBox [0 0 728.5 515.91]"));
|
||||
s.execute("pageLayout.pageSetup", json!({"paper": "B5", "orientation": "portrait"})).unwrap();
|
||||
let (pdf, _) = pdf_of(&mut s, json!({"fitToPage": true}));
|
||||
assert!(contains(&pdf, "/MediaBox [0 0 515.91 728.5]"));
|
||||
// `pageLayout.size` still takes any name, and an unknown one prints on Letter as before.
|
||||
s.execute("pageLayout.size", json!({"paper": "Napkin"})).unwrap();
|
||||
let pv = s.execute("file.printPreview", json!({})).unwrap();
|
||||
assert!(close(&pv["pageList"][0]["width"], 612.0) && close(&pv["pageList"][0]["height"], 792.0));
|
||||
// Page Setup refuses an unknown name.
|
||||
assert!(s.execute("pageLayout.pageSetup", json!({"paper": "Napkin"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_settings_never_panic_and_cap_pages() {
|
||||
let mut s = s();
|
||||
|
||||
@@ -35,7 +35,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}),
|
||||
cmd!("view.zoomToSelection", "Zoom to Selection", ["View", "Zoom"], None, "{viewWidth?, viewHeight?}", has_doc, zoom_selection),
|
||||
cmd!("pageLayout.orientation", "Orientation", ["Page Layout", "Page Setup"], None, "{orientation: portrait|landscape}", has_doc, orientation),
|
||||
cmd!("pageLayout.size", "Size", ["Page Layout", "Page Setup"], None, "{paper: Letter|Legal|A4|A3|A5|Tabloid|Executive}", has_doc, paper),
|
||||
cmd!(
|
||||
"pageLayout.size",
|
||||
"Size",
|
||||
["Page Layout", "Page Setup"],
|
||||
None,
|
||||
"{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)}",
|
||||
has_doc,
|
||||
paper
|
||||
),
|
||||
cmd!(
|
||||
"pageLayout.margins",
|
||||
"Margins",
|
||||
|
||||
@@ -1663,7 +1663,29 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) {
|
||||
let s = big_button(ui, Icon::Paper, "Size", "Paper Size", true);
|
||||
control_keytip(app, ui, &s, "pageLayout.size");
|
||||
egui::Popup::menu(&s).show(|ui| {
|
||||
for p in ["Letter", "Legal", "Tabloid", "Executive", "A3", "A4", "A5"] {
|
||||
// B4/B5/B6 are the JIS sizes Japanese Excel uses; the ISO ones stay selectable by name.
|
||||
for p in [
|
||||
"Letter",
|
||||
"Legal",
|
||||
"Tabloid",
|
||||
"Executive",
|
||||
"A3",
|
||||
"A4",
|
||||
"A5",
|
||||
"A6",
|
||||
"B4",
|
||||
"B5",
|
||||
"B6",
|
||||
"Japanese Postcard",
|
||||
"Japanese Double Postcard",
|
||||
"Japanese Envelope #3",
|
||||
"Japanese Envelope #4",
|
||||
"Japanese Envelope Kaku #2",
|
||||
"Japanese Envelope Kaku #3",
|
||||
"B4 (ISO)",
|
||||
"B5 (ISO)",
|
||||
"B6 (ISO)",
|
||||
] {
|
||||
if ui.button(p).clicked() {
|
||||
act(app, "pageLayout.size", json!({"paper": p}));
|
||||
}
|
||||
|
||||
@@ -64,8 +64,10 @@ pub const INDEXED: [u32; 64] = [
|
||||
0x003366, 0x339966, 0x003300, 0x333300, 0x993300, 0x993366, 0x333399, 0x333333, // 56-63
|
||||
];
|
||||
|
||||
/// Paper sizes (`pageSetup paperSize`) we name.
|
||||
const PAPERS: [(u32, &str); 14] = [
|
||||
/// Paper sizes (`pageSetup paperSize`) we name. ID 12/13 are the JIS B4/B5 that
|
||||
/// `pageLayout.pageSetup` calls `B4`/`B5`; 42 is ISO B4, and 43–88 are the Japanese postcards,
|
||||
/// envelopes and B6. The engine's `known_paper` uses the same names and sizes.
|
||||
const PAPERS: [(u32, &str); 23] = [
|
||||
(1, "Letter"),
|
||||
(3, "Tabloid"),
|
||||
(4, "Ledger"),
|
||||
@@ -80,14 +82,40 @@ const PAPERS: [(u32, &str); 14] = [
|
||||
(20, "Envelope #10"),
|
||||
(27, "Envelope DL"),
|
||||
(34, "Envelope B5"),
|
||||
(42, "B4 (ISO)"),
|
||||
(43, "Japanese Postcard"),
|
||||
(69, "Japanese Double Postcard"),
|
||||
(70, "A6"),
|
||||
(71, "Japanese Envelope Kaku #2"),
|
||||
(72, "Japanese Envelope Kaku #3"),
|
||||
(73, "Japanese Envelope #3"),
|
||||
(74, "Japanese Envelope #4"),
|
||||
(88, "B6"),
|
||||
];
|
||||
|
||||
pub fn paper_name(id: u32) -> Option<&'static str> {
|
||||
PAPERS.iter().find(|(i, _)| *i == id).map(|(_, n)| *n)
|
||||
}
|
||||
|
||||
/// The file ID for a paper name. Spelling is loose like `known_paper`'s — spaces, hyphens,
|
||||
/// underscores and parentheses don't matter, and the JIS/ISO and Japanese-envelope spellings the
|
||||
/// page-setup commands take are folded onto the name in [`PAPERS`].
|
||||
pub fn paper_id(name: &str) -> Option<u32> {
|
||||
PAPERS.iter().find(|(_, n)| n.eq_ignore_ascii_case(name.trim())).map(|(i, _)| *i)
|
||||
let norm = |s: &str| s.trim().to_ascii_lowercase().replace([' ', '-', '_', '(', ')'], "");
|
||||
let normalized = norm(name);
|
||||
let alias = match normalized.as_str() {
|
||||
"b4jis" => "b4",
|
||||
"b5jis" => "b5",
|
||||
"b6jis" => "b6",
|
||||
"postcard" => "japanesepostcard",
|
||||
"doublepostcard" => "japanesedoublepostcard",
|
||||
"chou#3" => "japaneseenvelope#3",
|
||||
"chou#4" => "japaneseenvelope#4",
|
||||
"kaku#2" => "japaneseenvelopekaku#2",
|
||||
"kaku#3" => "japaneseenvelopekaku#3",
|
||||
other => other,
|
||||
};
|
||||
PAPERS.iter().find(|(_, n)| norm(n) == alias).map(|(i, _)| *i)
|
||||
}
|
||||
|
||||
/// Functions introduced after Excel 2007 that files store with the `_xlfn.` prefix. Entries with
|
||||
@@ -339,5 +367,26 @@ mod tests {
|
||||
assert_eq!(builtin_id(builtin_format(5).unwrap()), None);
|
||||
assert_eq!(paper_name(9), Some("A4"));
|
||||
assert_eq!(paper_id("a4"), Some(9));
|
||||
// 12/13 are the JIS B4/B5 Japanese Excel uses; 42 is ISO B4.
|
||||
assert_eq!(paper_name(12), Some("B4"));
|
||||
assert_eq!(paper_id("b5"), Some(13));
|
||||
assert_eq!(paper_id("B5 (JIS)"), Some(13));
|
||||
assert_eq!(paper_id("B4 (ISO)"), Some(42));
|
||||
// The Japanese papers page setup names.
|
||||
assert_eq!(paper_name(43), Some("Japanese Postcard"));
|
||||
assert_eq!(paper_name(88), Some("B6"));
|
||||
for (name, id) in [
|
||||
("Japanese Postcard", 43),
|
||||
("japanese double postcard", 69),
|
||||
("A6", 70),
|
||||
("Japanese Envelope Kaku #2", 71),
|
||||
("kaku #3", 72),
|
||||
("Japanese Envelope #3", 73),
|
||||
("chou #4", 74),
|
||||
("B6 (JIS)", 88),
|
||||
] {
|
||||
assert_eq!(paper_id(name), Some(id), "{name}");
|
||||
}
|
||||
assert_eq!(paper_id("Napkin"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1165,3 +1165,35 @@ fn doc_props_keep_boundary_whitespace() {
|
||||
let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert_eq!(back.props, wb.props);
|
||||
}
|
||||
|
||||
/// Japanese paper sizes survive an XLSX save and reopen. IDs 12/13 are the JIS B4/B5; the
|
||||
/// Japanese postcards and envelopes have their own IDs, and `B4 (ISO)` keeps ISO B4. The ISO B5/B6
|
||||
/// names only exist in the engine: ECMA-376 has no `paperSize` id for them (only ISO B4), so they
|
||||
/// are not part of the round-trip.
|
||||
#[test]
|
||||
fn japanese_paper_sizes_round_trip() {
|
||||
for paper in [
|
||||
"B4",
|
||||
"B5",
|
||||
"B6",
|
||||
"B4 (ISO)",
|
||||
"A6",
|
||||
"Japanese Postcard",
|
||||
"Japanese Double Postcard",
|
||||
"Japanese Envelope #3",
|
||||
"Japanese Envelope #4",
|
||||
"Japanese Envelope Kaku #2",
|
||||
"Japanese Envelope Kaku #3",
|
||||
] {
|
||||
let mut wb = Workbook::new();
|
||||
wb.sheet_mut(0).unwrap().print.paper = paper.into();
|
||||
let (back, report) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert!(report.warnings.is_empty(), "{paper}: {:?}", report.warnings);
|
||||
assert_eq!(back.sheet(0).unwrap().print.paper, paper);
|
||||
}
|
||||
// An alias is written as the file's own name and read back as that name.
|
||||
let mut wb = Workbook::new();
|
||||
wb.sheet_mut(0).unwrap().print.paper = "B5 (JIS)".into();
|
||||
let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert_eq!(back.sheet(0).unwrap().print.paper, "B5");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user