Skip to main content

jw_hwp_core/
table.rs

1//! HWP table control parsing: HWPTAG_TABLE payload + per-cell LIST_HEADER.
2
3use crate::error::Error;
4
5#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
6pub struct Table {
7    pub id: String,
8    pub rows: u16,
9    pub cols: u16,
10    pub caption: Option<String>,
11    pub cells: Vec<Vec<Option<Cell>>>,
12}
13
14#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
15pub struct Cell {
16    pub col: u16,
17    pub row: u16,
18    pub col_span: u16,
19    pub row_span: u16,
20    pub text: String,
21    pub paragraphs: Vec<String>,
22}
23
24#[derive(Debug, Clone)]
25pub(crate) struct TableProps {
26    pub rows: u16,
27    pub cols: u16,
28}
29
30pub(crate) fn parse_table_payload(p: &[u8]) -> Result<TableProps, Error> {
31    if p.len() < 22 {
32        return Err(Error::Record(format!(
33            "HWPTAG_TABLE too short: {}",
34            p.len()
35        )));
36    }
37    let rows = u16::from_le_bytes(p[4..6].try_into().unwrap());
38    let cols = u16::from_le_bytes(p[6..8].try_into().unwrap());
39    // Sanity: full length should be 22 + 2*rows + 10*zones; we don't need zones here.
40    Ok(TableProps { rows, cols })
41}
42
43#[derive(Debug, Clone)]
44pub(crate) struct CellListHeader {
45    pub para_count: i16,
46    pub col: u16,
47    pub row: u16,
48    pub col_span: u16,
49    pub row_span: u16,
50}
51
52pub(crate) fn parse_cell_list_header(p: &[u8]) -> Result<CellListHeader, Error> {
53    if p.len() < 34 {
54        return Err(Error::Record(format!(
55            "cell LIST_HEADER too short: {}",
56            p.len()
57        )));
58    }
59    // Base LIST_HEADER = 8 bytes (INT16 para_count, UINT32 props, UINT16 unknown/list_id),
60    // followed by 26 bytes of cell metadata.
61    let para_count = i16::from_le_bytes(p[0..2].try_into().unwrap());
62    let col = u16::from_le_bytes(p[8..10].try_into().unwrap());
63    let row = u16::from_le_bytes(p[10..12].try_into().unwrap());
64    let col_span = u16::from_le_bytes(p[12..14].try_into().unwrap());
65    let row_span = u16::from_le_bytes(p[14..16].try_into().unwrap());
66    Ok(CellListHeader {
67        para_count,
68        col,
69        row,
70        col_span,
71        row_span,
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    fn build_table_props(rows: u16, cols: u16, zones: u16) -> Vec<u8> {
80        let mut p = Vec::new();
81        p.extend_from_slice(&0u32.to_le_bytes()); // props
82        p.extend_from_slice(&rows.to_le_bytes());
83        p.extend_from_slice(&cols.to_le_bytes());
84        p.extend_from_slice(&0u16.to_le_bytes()); // cell_spacing
85        p.extend_from_slice(&[0u8; 8]); // inner margins
86        for _ in 0..rows {
87            p.extend_from_slice(&1000u16.to_le_bytes());
88        }
89        p.extend_from_slice(&0u16.to_le_bytes()); // border_fill_id
90        p.extend_from_slice(&zones.to_le_bytes());
91        for _ in 0..zones {
92            p.extend_from_slice(&[0u8; 10]);
93        }
94        p
95    }
96
97    #[test]
98    fn parses_2x3_no_zones() {
99        let p = build_table_props(2, 3, 0);
100        assert_eq!(p.len(), 22 + 4);
101        let tp = parse_table_payload(&p).unwrap();
102        assert_eq!((tp.rows, tp.cols), (2, 3));
103    }
104
105    #[test]
106    fn parses_with_zones() {
107        let p = build_table_props(3, 1, 1);
108        assert_eq!(p.len(), 22 + 6 + 10);
109        let tp = parse_table_payload(&p).unwrap();
110        assert_eq!((tp.rows, tp.cols), (3, 1));
111    }
112
113    #[test]
114    fn rejects_truncated() {
115        let p = vec![0u8; 10];
116        assert!(parse_table_payload(&p).is_err());
117    }
118
119    #[test]
120    fn parses_cell_list_header() {
121        let mut p = Vec::new();
122        p.extend_from_slice(&1i16.to_le_bytes()); // para_count
123        p.extend_from_slice(&[0u8; 4]); // props
124        p.extend_from_slice(&[0u8; 2]); // unknown/list_id
125        p.extend_from_slice(&2u16.to_le_bytes()); // col
126        p.extend_from_slice(&1u16.to_le_bytes()); // row
127        p.extend_from_slice(&1u16.to_le_bytes()); // col_span
128        p.extend_from_slice(&1u16.to_le_bytes()); // row_span
129        p.extend_from_slice(&[0u8; 18]); // rest of cell meta
130        let h = parse_cell_list_header(&p).unwrap();
131        assert_eq!((h.col, h.row, h.col_span, h.row_span), (2, 1, 1, 1));
132        assert_eq!(h.para_count, 1);
133    }
134}