Skip to main content

extend_docx/doc/
sep.rs

1//! Section properties (SEP): parsed from SEPX grpprls located via PlcfSed,
2//! emitted as OOXML `<w:sectPr>`. Page size/margins and the document grid
3//! (sprmSClm / sprmSDyaLinePitch) are the layout-fidelity-critical fields.
4
5use super::fmt::nfc_to_num_fmt;
6use super::sprm::{self, Sprm};
7
8#[derive(Debug, Clone)]
9pub struct Sep {
10    pub bkc: u8,
11    pub f_title_page: bool,
12    pub ccol_m1: u16,
13    pub dxa_columns: u16,
14    pub f_evenly_spaced: bool,
15    pub col_widths: Vec<(u8, u16)>,
16    pub nfc_pgn: Option<u8>,
17    pub f_pgn_restart: bool,
18    pub pgn_start: u16,
19    pub xa_page: u32,
20    pub ya_page: u32,
21    pub orientation_landscape: bool,
22    pub dxa_left: u32,
23    pub dxa_right: u32,
24    pub dya_top: i32,
25    pub dya_bottom: i32,
26    pub dza_gutter: u32,
27    pub dya_hdr_top: u32,
28    pub dya_hdr_bottom: u32,
29    pub clm: u16,
30    pub dya_line_pitch: u16,
31    pub dxt_char_space: u32,
32}
33
34impl Default for Sep {
35    fn default() -> Sep {
36        // Word's defaults for a US Letter section.
37        Sep {
38            bkc: 2,
39            f_title_page: false,
40            ccol_m1: 0,
41            dxa_columns: 708,
42            f_evenly_spaced: true,
43            col_widths: Vec::new(),
44            nfc_pgn: None,
45            f_pgn_restart: false,
46            pgn_start: 1,
47            xa_page: 12240,
48            ya_page: 15840,
49            orientation_landscape: false,
50            dxa_left: 1800,
51            dxa_right: 1800,
52            dya_top: 1440,
53            dya_bottom: 1440,
54            dza_gutter: 0,
55            dya_hdr_top: 720,
56            dya_hdr_bottom: 720,
57            clm: 0,
58            dya_line_pitch: 0,
59            dxt_char_space: 0,
60        }
61    }
62}
63
64impl Sep {
65    pub fn apply_sepx(&mut self, grpprl: &[u8]) {
66        for sprm in sprm::iterate(grpprl) {
67            self.apply_sprm(&sprm);
68        }
69    }
70
71    fn apply_sprm(&mut self, sprm: &Sprm<'_>) {
72        match sprm.opcode {
73            0x3009 => self.bkc = sprm.byte(),
74            0x300A => self.f_title_page = sprm.byte() == 1,
75            0x500B => self.ccol_m1 = sprm.u16_operand(),
76            0x900C => self.dxa_columns = sprm.u16_operand(),
77            0x3005 => self.f_evenly_spaced = sprm.byte() == 1,
78            0xF203 => {
79                if sprm.operand.len() >= 3 {
80                    self.col_widths.push((
81                        sprm.operand[0],
82                        u16::from_le_bytes([sprm.operand[1], sprm.operand[2]]),
83                    ));
84                }
85            }
86            0x300E => self.nfc_pgn = Some(sprm.byte()),
87            0x3011 => self.f_pgn_restart = sprm.byte() == 1,
88            0x501C => self.pgn_start = sprm.u16_operand(),
89            0xB01F => self.xa_page = sprm.u16_operand() as u32,
90            0xB020 => self.ya_page = sprm.u16_operand() as u32,
91            0x301D => self.orientation_landscape = sprm.byte() == 2,
92            0xB021 => self.dxa_left = sprm.u16_operand() as u32,
93            0xB022 => self.dxa_right = sprm.u16_operand() as u32,
94            0x9023 => self.dya_top = sprm.i16_operand() as i32,
95            0x9024 => self.dya_bottom = sprm.i16_operand() as i32,
96            0xB025 => self.dza_gutter = sprm.u16_operand() as u32,
97            0xB017 => self.dya_hdr_top = sprm.u16_operand() as u32,
98            0xB018 => self.dya_hdr_bottom = sprm.u16_operand() as u32,
99            0x5032 => self.clm = sprm.u16_operand(),
100            0x9031 => self.dya_line_pitch = sprm.u16_operand(),
101            0x7030 => self.dxt_char_space = sprm.u32_operand(),
102            _ => {}
103        }
104    }
105
106    /// Emits the inner content of `<w:sectPr>`; header/footer references and
107    /// footnote settings are appended by the document builder.
108    pub fn to_sect_pr_inner_xml(&self) -> String {
109        let mut inner = String::new();
110
111        let break_type = match self.bkc {
112            0 => Some("continuous"),
113            1 => Some("nextColumn"),
114            3 => Some("evenPage"),
115            4 => Some("oddPage"),
116            _ => None, // 2 = nextPage, the default
117        };
118        if let Some(val) = break_type {
119            inner.push_str(&format!(r#"<w:type w:val="{val}"/>"#));
120        }
121
122        inner.push_str(&format!(
123            r#"<w:pgSz w:w="{}" w:h="{}"{}/>"#,
124            self.xa_page,
125            self.ya_page,
126            if self.orientation_landscape {
127                r#" w:orient="landscape""#
128            } else {
129                ""
130            }
131        ));
132        inner.push_str(&format!(
133            r#"<w:pgMar w:top="{}" w:right="{}" w:bottom="{}" w:left="{}" w:header="{}" w:footer="{}" w:gutter="{}"/>"#,
134            self.dya_top,
135            self.dxa_right,
136            self.dya_bottom,
137            self.dxa_left,
138            self.dya_hdr_top,
139            self.dya_hdr_bottom,
140            self.dza_gutter
141        ));
142
143        if self.nfc_pgn.is_some() || self.f_pgn_restart {
144            let mut pgn = String::from("<w:pgNumType");
145            if let Some(nfc) = self.nfc_pgn {
146                pgn.push_str(&format!(r#" w:fmt="{}""#, nfc_to_num_fmt(nfc)));
147            }
148            if self.f_pgn_restart {
149                pgn.push_str(&format!(r#" w:start="{}""#, self.pgn_start));
150            }
151            pgn.push_str("/>");
152            inner.push_str(&pgn);
153        }
154
155        if self.ccol_m1 > 0 {
156            let mut cols = format!(
157                r#"<w:cols w:num="{}" w:space="{}""#,
158                self.ccol_m1 + 1,
159                self.dxa_columns
160            );
161            if !self.f_evenly_spaced && !self.col_widths.is_empty() {
162                cols.push_str(r#" w:equalWidth="0">"#);
163                for (_, width) in &self.col_widths {
164                    cols.push_str(&format!(
165                        r#"<w:col w:w="{width}" w:space="{}"/>"#,
166                        self.dxa_columns
167                    ));
168                }
169                cols.push_str("</w:cols>");
170            } else {
171                cols.push_str("/>");
172            }
173            inner.push_str(&cols);
174        }
175
176        if self.f_title_page {
177            inner.push_str("<w:titlePg/>");
178        }
179
180        if self.clm != 0 || self.dya_line_pitch != 0 {
181            let grid_type = match self.clm {
182                1 => "linesAndChars",
183                2 => "lines",
184                3 => "snapToChars",
185                _ => "default",
186            };
187            let mut grid = format!(r#"<w:docGrid w:type="{grid_type}""#);
188            if self.dya_line_pitch != 0 {
189                grid.push_str(&format!(r#" w:linePitch="{}""#, self.dya_line_pitch));
190            }
191            if self.dxt_char_space != 0 {
192                grid.push_str(&format!(r#" w:charSpace="{}""#, self.dxt_char_space));
193            }
194            grid.push_str("/>");
195            inner.push_str(&grid);
196        }
197
198        inner
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn emits_page_size_and_grid() {
208        let grpprl: Vec<u8> = vec![
209            0x1F, 0xB0, 0xB0, 0x29, // xaPage 10672? -> 0x29B0 = 10672
210            0x20, 0xB0, 0x84, 0x3D, // yaPage 0x3D84 = 15748
211            0x32, 0x50, 0x01, 0x00, // clm = 1
212            0x31, 0x90, 0x6F, 0x01, // linePitch 367
213        ];
214        let mut sep = Sep::default();
215        sep.apply_sepx(&grpprl);
216        let xml = sep.to_sect_pr_inner_xml();
217        assert!(xml.contains(r#"<w:pgSz w:w="10672" w:h="15748"/>"#));
218        assert!(xml.contains(r#"<w:docGrid w:type="linesAndChars" w:linePitch="367"/>"#));
219    }
220
221    #[test]
222    fn continuous_break_and_columns() {
223        let grpprl: Vec<u8> = vec![
224            0x09, 0x30, 0x00, // bkc continuous
225            0x0B, 0x50, 0x01, 0x00, // 2 columns
226        ];
227        let mut sep = Sep::default();
228        sep.apply_sepx(&grpprl);
229        let xml = sep.to_sect_pr_inner_xml();
230        assert!(xml.contains(r#"<w:type w:val="continuous"/>"#));
231        assert!(xml.contains(r#"<w:cols w:num="2" w:space="708"/>"#));
232    }
233}