docxide-pdf 0.9.0

CLI and Library for converting DOCX files to PDF, matching Microsoft Word's output as closely as possible
Documentation
use std::collections::HashMap;
use std::io::Read;

use crate::model::{
    Alignment, CellBorders, CellMargins, CellVAlign, HorizontalPosition, LineSpacing, Paragraph,
    Table, TableCell, TablePosition, TableRow, VMerge,
};

use super::numbering::{self, parse_list_info};
use super::runs::parse_runs;
use super::styles::{self, TableBordersDef, parse_alignment, parse_line_spacing};
use super::{
    WML_NS, collect_block_nodes, parse_cell_border, parse_cell_border_left,
    parse_cell_border_right, parse_hex_color, twips_attr, twips_to_pts, wml, wml_attr,
};

pub(in crate::docx) fn parse_table_node<R: Read + std::io::Seek>(
    node: roxmltree::Node,
    styles: &styles::StylesInfo,
    theme: &styles::ThemeFonts,
    rels: &HashMap<String, String>,
    zip: &mut zip::ZipArchive<R>,
    numbering: &numbering::NumberingInfo,
    counters: &mut HashMap<(String, u8), u32>,
    last_seen_level: &mut HashMap<String, u8>,
) -> Table {
    let col_widths: Vec<f32> = wml(node, "tblGrid")
        .into_iter()
        .flat_map(|grid| grid.children())
        .filter(|n| n.tag_name().name() == "gridCol" && n.tag_name().namespace() == Some(WML_NS))
        .filter_map(|n| twips_attr(n, "w"))
        .collect();

    let tbl_pr = wml(node, "tblPr");
    let table_indent = tbl_pr
        .and_then(|pr| wml(pr, "tblInd"))
        .and_then(|ind| twips_attr(ind, "w"))
        .unwrap_or(0.0);

    let cell_margins = tbl_pr
        .and_then(|pr| wml(pr, "tblCellMar"))
        .map(|mar| CellMargins {
            top: wml(mar, "top")
                .and_then(|n| twips_attr(n, "w"))
                .unwrap_or(0.0),
            left: wml(mar, "left")
                .or_else(|| wml(mar, "start"))
                .and_then(|n| twips_attr(n, "w"))
                .unwrap_or(5.4),
            bottom: wml(mar, "bottom")
                .and_then(|n| twips_attr(n, "w"))
                .unwrap_or(0.0),
            right: wml(mar, "right")
                .or_else(|| wml(mar, "end"))
                .and_then(|n| twips_attr(n, "w"))
                .unwrap_or(5.4),
        })
        .unwrap_or_default();

    let table_position = tbl_pr.and_then(|pr| wml(pr, "tblpPr")).map(|tblp| {
        let v_anchor = match tblp.attribute((WML_NS, "vertAnchor")) {
            Some("page") => "page",
            Some("text") => "text",
            _ => "margin",
        };
        let h_anchor = match tblp.attribute((WML_NS, "horzAnchor")) {
            Some("page") => "page",
            Some("margin") => "margin",
            _ => "column",
        };
        let v_offset_pt = tblp
            .attribute((WML_NS, "tblpY"))
            .and_then(|v| v.parse::<f32>().ok())
            .map(twips_to_pts)
            .unwrap_or(0.0);
        let h_position = if let Some(spec) = tblp.attribute((WML_NS, "tblpXSpec")) {
            match spec {
                "center" => HorizontalPosition::AlignCenter,
                "right" => HorizontalPosition::AlignRight,
                _ => HorizontalPosition::AlignLeft,
            }
        } else {
            let offset = tblp
                .attribute((WML_NS, "tblpX"))
                .and_then(|v| v.parse::<f32>().ok())
                .map(twips_to_pts)
                .unwrap_or(0.0);
            HorizontalPosition::Offset(offset)
        };
        TablePosition {
            h_position,
            h_anchor,
            v_offset_pt,
            v_anchor,
        }
    });

    let has_tbl_style = tbl_pr.and_then(|pr| wml_attr(pr, "tblStyle")).is_some();
    let tbl_style_borders = tbl_pr
        .and_then(|pr| wml_attr(pr, "tblStyle"))
        .and_then(|id| styles.table_border_styles.get(id));

    let inline_tbl_borders =
        tbl_pr
            .and_then(|pr| wml(pr, "tblBorders"))
            .map(|bdr_node| TableBordersDef {
                top: parse_cell_border(bdr_node, "top"),
                bottom: parse_cell_border(bdr_node, "bottom"),
                left: parse_cell_border_left(bdr_node),
                right: parse_cell_border_right(bdr_node),
                inside_h: parse_cell_border(bdr_node, "insideH"),
                inside_v: parse_cell_border(bdr_node, "insideV"),
            });

    let effective_tbl_borders: Option<&TableBordersDef> =
        inline_tbl_borders.as_ref().or(tbl_style_borders);

    let tbl_rows: Vec<_> = collect_block_nodes(node)
        .into_iter()
        .filter(|n| n.tag_name().name() == "tr" && n.tag_name().namespace() == Some(WML_NS))
        .collect();
    let num_rows = tbl_rows.len();
    let num_cols = col_widths.len();

    let mut rows = Vec::new();
    for (ri, tr) in tbl_rows.iter().enumerate() {
        let tr_pr = wml(*tr, "trPr");
        let (row_height, height_exact) = tr_pr
            .and_then(|pr| wml(pr, "trHeight"))
            .map(|h| {
                let val = h
                    .attribute((WML_NS, "val"))
                    .and_then(|v| v.parse::<f32>().ok())
                    .map(twips_to_pts);
                let exact = h.attribute((WML_NS, "hRule")) == Some("exact");
                (val, exact)
            })
            .unwrap_or((None, false));

        let mut cells = Vec::new();
        let mut grid_col = 0usize;
        for tc in collect_block_nodes(*tr)
            .into_iter()
            .filter(|n| n.tag_name().name() == "tc" && n.tag_name().namespace() == Some(WML_NS))
        {
            let ci = grid_col;
            let tc_pr = wml(tc, "tcPr");
            let cell_width = tc_pr
                .and_then(|pr| wml(pr, "tcW"))
                .and_then(|w| twips_attr(w, "w"))
                .unwrap_or_else(|| col_widths.get(ci).copied().unwrap_or(72.0));

            let grid_span = tc_pr
                .and_then(|pr| wml(pr, "gridSpan"))
                .and_then(|n| n.attribute((WML_NS, "val")))
                .and_then(|v| v.parse::<u16>().ok())
                .unwrap_or(1);

            let v_merge = tc_pr
                .and_then(|pr| wml(pr, "vMerge"))
                .map(|n| match n.attribute((WML_NS, "val")) {
                    Some("restart") => VMerge::Restart,
                    _ => VMerge::Continue,
                })
                .unwrap_or(VMerge::None);

            let v_align = tc_pr
                .and_then(|pr| wml(pr, "vAlign"))
                .and_then(|n| n.attribute((WML_NS, "val")))
                .map(|v| match v {
                    "center" => CellVAlign::Center,
                    "bottom" => CellVAlign::Bottom,
                    _ => CellVAlign::Top,
                })
                .unwrap_or(CellVAlign::Top);

            let span_end = ci + grid_span as usize;

            let style_borders = effective_tbl_borders.map(|tb| CellBorders {
                top: if ri == 0 { tb.top } else { tb.inside_h },
                bottom: if ri == num_rows - 1 {
                    tb.bottom
                } else {
                    tb.inside_h
                },
                left: if ci == 0 { tb.left } else { tb.inside_v },
                right: if span_end >= num_cols {
                    tb.right
                } else {
                    tb.inside_v
                },
            });

            let borders = tc_pr
                .and_then(|pr| wml(pr, "tcBorders"))
                .map(|bdr| {
                    let fallback = style_borders.unwrap_or_default();
                    let top = parse_cell_border(bdr, "top");
                    let bottom = parse_cell_border(bdr, "bottom");
                    let left = parse_cell_border_left(bdr);
                    let right = parse_cell_border_right(bdr);
                    CellBorders {
                        top: if top.present { top } else { fallback.top },
                        bottom: if bottom.present {
                            bottom
                        } else {
                            fallback.bottom
                        },
                        left: if left.present { left } else { fallback.left },
                        right: if right.present { right } else { fallback.right },
                    }
                })
                .unwrap_or_else(|| style_borders.unwrap_or_default());

            let shading = tc_pr
                .and_then(|pr| wml(pr, "shd"))
                .and_then(|shd| shd.attribute((WML_NS, "fill")))
                .filter(|f| *f != "none")
                .and_then(parse_hex_color);

            let mut cell_paras = Vec::new();
            for p in tc
                .children()
                .filter(|n| n.tag_name().name() == "p" && n.tag_name().namespace() == Some(WML_NS))
            {
                let parsed = parse_runs(p, styles, theme, rels, zip, numbering);
                let ppr = wml(p, "pPr");
                let para_style_id = ppr
                    .and_then(|ppr| wml_attr(ppr, "pStyle"))
                    .unwrap_or(&styles.default_paragraph_style_id);
                let para_style = styles.paragraph_styles.get(para_style_id);
                let alignment = ppr
                    .and_then(|ppr| wml_attr(ppr, "jc"))
                    .map(parse_alignment)
                    .or_else(|| para_style.and_then(|s| s.alignment))
                    .unwrap_or(Alignment::Left);
                let inline_spacing = ppr.and_then(|ppr| wml(ppr, "spacing"));
                let line_spacing = inline_spacing
                    .and_then(|n| {
                        n.attribute((WML_NS, "line"))
                            .and_then(|v| v.parse::<f32>().ok())
                            .map(|line_val| parse_line_spacing(n, line_val))
                    })
                    .or_else(|| para_style.and_then(|s| s.line_spacing))
                    .or_else(|| {
                        if has_tbl_style {
                            Some(LineSpacing::Auto(1.0))
                        } else {
                            None
                        }
                    });
                let num_pr = ppr.and_then(|ppr| wml(ppr, "numPr"));
                let (mut indent_left, mut indent_hanging, list_label, list_label_font) =
                    parse_list_info(num_pr, numbering, counters, last_seen_level);
                let mut indent_first_line = 0.0f32;
                let mut indent_right = 0.0f32;
                if let Some(ind) = ppr.and_then(|ppr| wml(ppr, "ind")) {
                    if let Some(v) = twips_attr(ind, "start").or_else(|| twips_attr(ind, "left")) {
                        indent_left = v;
                    }
                    if let Some(v) = twips_attr(ind, "end").or_else(|| twips_attr(ind, "right")) {
                        indent_right = v;
                    }
                    if let Some(v) = twips_attr(ind, "hanging") {
                        indent_hanging = v;
                    }
                    if let Some(v) = twips_attr(ind, "firstLine") {
                        indent_first_line = v;
                    }
                }
                let space_before = inline_spacing
                    .and_then(|n| twips_attr(n, "before"))
                    .or_else(|| para_style.and_then(|s| s.space_before))
                    .unwrap_or(0.0);
                let space_after = inline_spacing
                    .and_then(|n| twips_attr(n, "after"))
                    .or_else(|| para_style.and_then(|s| s.space_after))
                    .unwrap_or_else(|| {
                        if has_tbl_style {
                            0.0
                        } else {
                            styles.defaults.space_after
                        }
                    });
                cell_paras.push(Paragraph {
                    runs: parsed.runs,
                    alignment,
                    indent_left,
                    indent_right,
                    indent_hanging,
                    indent_first_line,
                    list_label,
                    list_label_font,
                    line_spacing,
                    space_before,
                    space_after,
                    extra_line_breaks: parsed.line_break_count,
                    ..Paragraph::default()
                });
            }
            cells.push(TableCell {
                width: cell_width,
                paragraphs: cell_paras,
                borders,
                shading,
                grid_span,
                v_merge,
                v_align,
            });
            grid_col += grid_span as usize;
        }
        rows.push(TableRow {
            cells,
            height: row_height,
            height_exact,
        });
    }
    Table {
        col_widths,
        rows,
        table_indent,
        cell_margins,
        position: table_position,
    }
}