k2f_docx 0.2.3

K2F lock → DOCX exporter
Documentation
use crate::ir::LineDash;
use crate::shape::{round_rect_adj, shapes_from_box};
use crate::DocxError;
use k2f_core::{
    Blur, BlurRef, Border, BorderEdge, BorderStyle, BoxDecoration, Fill, FillRef, GradientStop,
    LinearGradient, Pt, Rect, Shadow, ShadowRef,
};

fn rect() -> Rect {
    Rect {
        x: Pt(10_000),
        y: Pt(20_000),
        width: Pt(100_000),
        height: Pt(50_000),
    }
}

fn page() -> (Pt, Pt) {
    (Pt(595_000), Pt(842_000))
}

#[test]
fn skips_shadow_rule_alpha_empty() {
    let (w, h) = page();
    let mut dec = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#FFFFFF".into(),
        })),
        shadow: Some(ShadowRef::Inline(Shadow { layers: vec![] })),
        ..Default::default()
    };
    assert!(shapes_from_box("card", &rect(), &dec, w, h, 0, "FFFFFF")
        .unwrap()
        .is_empty());
    dec.shadow = None;
    assert!(
        shapes_from_box("eq::rule_1", &rect(), &dec, w, h, 0, "FFFFFF")
            .unwrap()
            .is_empty()
    );
    dec.background = Some(FillRef::Inline(Fill::Solid {
        color: "#FFFFFF80".into(),
    }));
    let glass = shapes_from_box("glass", &rect(), &dec, w, h, 0, "FFFFFF").unwrap();
    assert_eq!(glass.len(), 1);
    assert_eq!(glass[0].fill_hex.as_deref(), Some("FFFFFF"));
    assert_eq!(glass[0].fill_alpha, 0x80);
    assert!(!glass[0].behind_doc);
    assert!(shapes_from_box(
        "empty",
        &rect(),
        &BoxDecoration::default(),
        w,
        h,
        0,
        "FFFFFF"
    )
    .unwrap()
    .is_empty());
}

#[test]
fn unresolved_fill_ref_fails() {
    let (w, h) = page();
    let dec = BoxDecoration {
        background: Some(FillRef::Ref("missing_surface".into())),
        ..Default::default()
    };
    let err = shapes_from_box("card", &rect(), &dec, w, h, 0, "FFFFFF").unwrap_err();
    match err {
        DocxError::Write(msg) => assert!(msg.contains("unresolved fill ref")),
        other => panic!("expected Write, got {other:?}"),
    }
}

#[test]
fn adj_clamped() {
    assert_eq!(round_rect_adj(0, 10_000, 10_000), 0);
    assert_eq!(round_rect_adj(1_000, 10_000, 20_000), 10_000);
    assert_eq!(round_rect_adj(10_000, 10_000, 10_000), 50_000);
}

#[test]
fn partial_border_emits_edge_bars_not_four_sided_ln() {
    let (w, h) = page();
    let dec = BoxDecoration {
        border: Some(Border {
            width_pt: 1_000,
            color: "#111111".into(),
            edges: vec![BorderEdge::Bottom],
            style: BorderStyle::Solid,
        }),
        ..Default::default()
    };
    let boxes = shapes_from_box("rule", &rect(), &dec, w, h, 10, "FFFFFF").unwrap();
    assert_eq!(boxes.len(), 1);
    assert!(boxes[0].node_id.ends_with("::edge_bottom"));
    assert_eq!(boxes[0].fill_hex.as_deref(), Some("111111"));
    assert!(boxes[0].line_hex.is_none());
}

fn four_edge(style: BorderStyle) -> Border {
    Border {
        width_pt: 1_000,
        color: "#FF0000".into(),
        edges: vec![
            BorderEdge::Top,
            BorderEdge::Right,
            BorderEdge::Bottom,
            BorderEdge::Left,
        ],
        style,
    }
}

#[test]
fn skips_gradient_and_blur() {
    let (w, h) = page();
    let grad = BoxDecoration {
        background: Some(FillRef::Inline(Fill::LinearGradient {
            value: LinearGradient::Linear {
                angle_degrees: 90,
                stops: vec![
                    GradientStop {
                        pos: 0,
                        color: "#000000".into(),
                    },
                    GradientStop {
                        pos: 1000,
                        color: "#FFFFFF".into(),
                    },
                ],
            },
        })),
        ..Default::default()
    };
    let boxes = shapes_from_box("card", &rect(), &grad, w, h, 0, "FFFFFF").unwrap();
    assert_eq!(boxes.len(), 1);
    assert!(
        boxes[0].gradient.is_some(),
        "linear gradient is a native fill"
    );
    assert!(
        !boxes[0].behind_doc,
        "non-page gradient stays in front of the page fill"
    );
    let full = Rect {
        x: Pt(0),
        y: Pt(0),
        width: w,
        height: h,
    };
    let page_grad = shapes_from_box("snap.shell", &full, &grad, w, h, 0, "FFFFFF").unwrap();
    assert_eq!(page_grad.len(), 1);
    assert!(
        page_grad[0].behind_doc,
        "full-page gradient must sit behind later text"
    );
    let blur = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#FFFFFF".into(),
        })),
        blur: Some(BlurRef::Inline(Blur { radius_pt: 8_000 })),
        ..Default::default()
    };
    assert!(shapes_from_box("glass", &rect(), &blur, w, h, 0, "FFFFFF")
        .unwrap()
        .is_empty());
}

#[test]
fn four_solid_edges_use_ln() {
    let (w, h) = page();
    let dec = BoxDecoration {
        border: Some(four_edge(BorderStyle::Solid)),
        ..Default::default()
    };
    assert!(dec.border.as_ref().unwrap().is_full_rect_stroke());
    let boxes = shapes_from_box("card", &rect(), &dec, w, h, 10, "FFFFFF").unwrap();
    assert_eq!(boxes.len(), 1);
    assert_eq!(boxes[0].line_hex.as_deref(), Some("FF0000"));
    assert_eq!(boxes[0].line_dash, LineDash::Solid);
    assert!(!boxes[0].node_id.contains("::edge_"));
}

#[test]
fn large_fill_keeps_stroke_in_front() {
    let (w, h) = page();
    let frame = Rect {
        x: Pt(56_667),
        y: Pt(14_115),
        width: Pt(524_166),
        height: Pt(813_770),
    };
    let dec = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#FFFFFF".into(),
        })),
        border: Some(four_edge(BorderStyle::Solid)),
        ..Default::default()
    };
    let boxes = shapes_from_box("main_frame", &frame, &dec, w, h, 150, "FFFFFF").unwrap();
    assert_eq!(boxes.len(), 2, "fill and stroke must be separate shapes");
    assert!(boxes[0].behind_doc, "large fill stays behind text");
    assert!(boxes[0].line_hex.is_none());
    assert_eq!(boxes[0].fill_hex.as_deref(), Some("FFFFFF"));
    assert!(
        !boxes[1].behind_doc,
        "frame stroke must stay in front of w:background"
    );
    assert!(boxes[1].fill_hex.is_none());
    assert_eq!(boxes[1].line_hex.as_deref(), Some("FF0000"));
    assert!(boxes[1].node_id.ends_with("::stroke"));
}

#[test]
fn four_dashed_edges_use_ln_not_bars() {
    let (w, h) = page();
    let border = four_edge(BorderStyle::Dashed);
    assert!(!border.is_full_rect_stroke());
    let dec = BoxDecoration {
        border: Some(border),
        ..Default::default()
    };
    let boxes = shapes_from_box("card", &rect(), &dec, w, h, 10, "FFFFFF").unwrap();
    assert_eq!(
        boxes.len(),
        1,
        "dashed rectangle must stay one a:ln, not four bars"
    );
    assert_eq!(boxes[0].line_dash, LineDash::Dash);
    assert_eq!(boxes[0].line_hex.as_deref(), Some("FF0000"));
}

#[test]
fn page_background_is_behind_doc() {
    let (w, h) = page();
    let full = Rect {
        x: Pt(0),
        y: Pt(0),
        width: w,
        height: h,
    };
    let dec = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#FFFFFF".into(),
        })),
        ..Default::default()
    };
    let bg = shapes_from_box("root::page_0::background", &full, &dec, w, h, 0, "FFFFFF").unwrap();
    assert_eq!(bg.len(), 1);
    assert!(bg[0].behind_doc);
    let cell = shapes_from_box("invoice.th.item", &rect(), &dec, w, h, 10, "FFFFFF").unwrap();
    assert_eq!(cell.len(), 1);
    assert!(
        cell[0].behind_doc,
        "paper-colored large fills must sit behind text/pictures"
    );
    let rule_rect = Rect {
        x: Pt(0),
        y: Pt(0),
        width: Pt(114_000),
        height: Pt(1_000),
    };
    let rule = shapes_from_box("addr_line", &rule_rect, &dec, w, h, 10, "FFFFFF").unwrap();
    assert_eq!(rule.len(), 1);
    assert!(
        !rule[0].behind_doc,
        "1 pt rules must stay in front, got behind_doc={}",
        rule[0].behind_doc
    );
    let bar_rect = Rect {
        x: Pt(0),
        y: Pt(0),
        width: Pt(6_000),
        height: Pt(25_000),
    };
    let bar = shapes_from_box("bar.magenta", &bar_rect, &dec, w, h, 10, "FFFFFF").unwrap();
    assert_eq!(bar.len(), 1);
    assert!(
        !bar[0].behind_doc,
        "few-pt accent bars must stay in front, got behind_doc={}",
        bar[0].behind_doc
    );
}

#[test]
fn contrasting_large_fill_stays_in_front() {
    let (w, h) = page();
    let body = Rect {
        x: Pt(0),
        y: Pt(170_000),
        width: w,
        height: Pt(672_000),
    };
    let blue = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#002AA6".into(),
        })),
        ..Default::default()
    };
    let boxes = shapes_from_box("body", &body, &blue, w, h, 50, "FFFFFF").unwrap();
    assert_eq!(boxes.len(), 1);
    assert!(
        !boxes[0].behind_doc,
        "cover-page body must stay in front of white w:background, got behind_doc"
    );
    let stripe = Rect {
        x: Pt(0),
        y: Pt(153_000),
        width: w,
        height: Pt(17_000),
    };
    let yellow = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#F2CB00".into(),
        })),
        ..Default::default()
    };
    let bar = shapes_from_box("stripe", &stripe, &yellow, w, h, 40, "FFFFFF").unwrap();
    assert_eq!(bar.len(), 1);
    assert!(
        !bar[0].behind_doc,
        "17pt contrasting band must stay visible, got behind_doc"
    );
}

#[test]
fn corner_radius_recorded() {
    let (w, h) = page();
    let dec = BoxDecoration {
        background: Some(FillRef::Inline(Fill::Solid {
            color: "#1A73E8".into(),
        })),
        corner_radius_pt: Some(8_000),
        ..Default::default()
    };
    let boxes = shapes_from_box("card", &rect(), &dec, w, h, 0, "FFFFFF").unwrap();
    assert_eq!(boxes[0].corner_emu, crate::coord::millipt_to_emu(8_000));
}