k2f_docx 0.2.2

K2F lock → DOCX exporter
Documentation
use crate::geo::{find_geo, page_bg_hex};
use crate::ir::PageElement;
use crate::picture::picture_from_draw;
use crate::shape::shapes_from_box;
use crate::text::{has_page_tokens, textbox_from_draw_ctx, FontCtx};
use crate::DocxError;
use k2f_core::{for_each_node, LockFile, PaintOp, RunningBlockNode, RunningBlockPosition};
use k2f_paint::OpenedDocument;
use std::collections::{BTreeMap, HashSet};

/// Node ids whose running-header/footer text becomes Word PAGE/NUMPAGES
/// fields. Those stay in header1.xml / footer1.xml and are skipped in the
/// body so they are not duplicated as "Page 1 of 3".
///
/// Other running paint (logos, static labels) is drawn in the body at lock
/// coordinates. LibreOffice Writer does not paint header/footer parts when
/// `pgMar` header/footer is 0 (required so lock geometry maps onto the page).
pub(crate) fn running_field_ids(running: &[RunningBlockNode]) -> HashSet<String> {
    let mut ids = HashSet::new();
    for rb in running {
        for_each_node(&rb.node, &mut |n| {
            if let Some(t) = k2f_core::node_text(n) {
                if has_page_tokens(t) {
                    ids.insert(n.id.clone());
                }
            }
        });
    }
    ids
}

pub(crate) fn collect_running(
    doc: &OpenedDocument,
    lock: &LockFile,
    fonts: &FontCtx,
    ids: &HashSet<String>,
    assets: &BTreeMap<String, Vec<u8>>,
    media_n: &mut u32,
) -> Result<(Vec<PageElement>, Vec<PageElement>), DocxError> {
    let mut header = Vec::new();
    let mut footer = Vec::new();
    let mut seen = HashSet::new();
    let root = doc.semantic_root();
    let running = doc.running_blocks();
    let Some(page) = lock.geometry.pages.first() else {
        return Ok((header, footer));
    };
    let Some(plan) = lock
        .render_plan
        .pages
        .iter()
        .find(|p| p.index == page.index)
    else {
        return Ok((header, footer));
    };
    let paper = page_bg_hex(page, &plan.ops);
    for (i, op) in plan.ops.iter().enumerate() {
        let Some(node_id) = op_node_id(op) else {
            continue;
        };
        if !ids.contains(node_id) {
            continue;
        }
        let kind = op_kind(op);
        if !seen.insert((kind, node_id.to_string())) {
            continue;
        }
        let Some(pos) = position_of(running, node_id) else {
            continue;
        };
        let rel = u32::try_from(i.saturating_mul(10)).unwrap_or(u32::MAX);
        let mut els = Vec::new();
        match op {
            PaintOp::DrawText { rect, runs, .. } => {
                let Some(node) = k2f_core::find_in_trees(root, running, node_id) else {
                    continue;
                };
                let geo = find_geo(&page.root, node_id);
                if let Some(tb) = textbox_from_draw_ctx(node, rect, runs, geo, fonts, rel) {
                    els.push(PageElement::TextBox(tb));
                }
            }
            PaintOp::DrawBox {
                rect, decoration, ..
            } => {
                els.extend(
                    shapes_from_box(
                        node_id,
                        rect,
                        decoration,
                        page.width,
                        page.height,
                        rel,
                        &paper,
                    )?
                    .into_iter()
                    .map(PageElement::Shape),
                );
            }
            PaintOp::DrawImage { rect, src, .. } => {
                let pic = picture_from_draw(node_id, rect, src, assets, *media_n, rel)?;
                *media_n = media_n.saturating_add(1);
                els.push(PageElement::Picture(pic));
            }
            _ => continue,
        }
        let dest = match pos {
            RunningBlockPosition::Header => &mut header,
            RunningBlockPosition::Footer => &mut footer,
        };
        dest.extend(els);
    }
    Ok((header, footer))
}

fn op_node_id(op: &PaintOp) -> Option<&str> {
    match op {
        PaintOp::DrawText { node_id, .. }
        | PaintOp::DrawBox { node_id, .. }
        | PaintOp::DrawImage { node_id, .. }
        | PaintOp::BackdropBlur { node_id, .. }
        | PaintOp::DrawTableReference { node_id, .. } => Some(node_id.as_str()),
        PaintOp::Unknown => None,
    }
}

fn op_kind(op: &PaintOp) -> u8 {
    match op {
        PaintOp::DrawText { .. } => 0,
        PaintOp::DrawBox { .. } => 1,
        PaintOp::DrawImage { .. } => 2,
        _ => 3,
    }
}

fn position_of(running: &[RunningBlockNode], id: &str) -> Option<RunningBlockPosition> {
    running
        .iter()
        .find_map(|rb| k2f_core::find_node(&rb.node, id).map(|_| rb.position))
}