From b56b5858ad1501dc3055379dc597f745e940bde8 Mon Sep 17 00:00:00 2001 From: Yonghye Kwon Date: Thu, 16 Jul 2026 20:01:09 +0900 Subject: [PATCH] fix(docx): preserve positioned footer layout Signed-off-by: Yonghye Kwon --- assets/bugfixes/issue-204/after.jpg | Bin 0 -> 25964 bytes assets/bugfixes/issue-204/before.jpg | Bin 0 -> 24899 bytes assets/bugfixes/issue-204/gt.jpg | Bin 0 -> 27358 bytes crates/office2pdf/src/ir/elements.rs | 50 ++- crates/office2pdf/src/ir/elements_tests.rs | 6 + crates/office2pdf/src/lib_render_tests.rs | 6 + crates/office2pdf/src/parser/docx_sections.rs | 134 +++++++- crates/office2pdf/src/parser/xlsx_hf.rs | 7 +- crates/office2pdf/src/render/font_subst.rs | 5 +- crates/office2pdf/src/render/typst_gen.rs | 295 +++++++++++++++--- .../src/render/typst_gen_page_misc_tests.rs | 122 ++++++++ crates/office2pdf/tests/docx_fixtures.rs | 54 +++- 12 files changed, 624 insertions(+), 55 deletions(-) create mode 100644 assets/bugfixes/issue-204/after.jpg create mode 100644 assets/bugfixes/issue-204/before.jpg create mode 100644 assets/bugfixes/issue-204/gt.jpg diff --git a/assets/bugfixes/issue-204/after.jpg b/assets/bugfixes/issue-204/after.jpg new file mode 100644 index 0000000000000000000000000000000000000000..577fcee5efa3584993ab298a52677965f7a0185a GIT binary patch literal 25964 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 DC|}at literal 0 HcmV?d00001 diff --git a/assets/bugfixes/issue-204/gt.jpg b/assets/bugfixes/issue-204/gt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e50081960189696f76ebda182dcdb0623acc0e9a GIT binary patch literal 27358 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 tB07XnWKNU;H0nm76f9IK1#$8d_*&>zh^Dur0~, + /// Distance in points from the page edge, as specified by the section page margins. + pub distance_from_edge: Option, } /// A paragraph within a header or footer. @@ -13,6 +15,50 @@ pub struct HeaderFooter { pub struct HeaderFooterParagraph { pub style: ParagraphStyle, pub elements: Vec, + pub border: Option, + pub frame: Option, +} + +/// Page- or margin-relative positioning for a header/footer paragraph frame. +#[derive(Debug, Clone, PartialEq)] +pub struct HeaderFooterFrame { + pub x: Option, + pub y: Option, + pub width: Option, + pub height: Option, + pub horizontal_anchor: FrameAnchor, + pub vertical_anchor: FrameAnchor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum FrameAnchor { + Page, + Margin, + #[default] + Text, +} + +/// A position-relative tab (`w:ptab`) inside header/footer content. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PositionedTab { + pub alignment: PositionedTabAlignment, + pub relative_to: PositionedTabRelativeTo, + pub leader: TabLeader, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum PositionedTabAlignment { + Center, + #[default] + Left, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum PositionedTabRelativeTo { + Indent, + #[default] + Margin, } /// An inline element within a header or footer paragraph. @@ -26,6 +72,8 @@ pub enum HFInline { PageNumber, /// Total page count field. TotalPages, + /// Alignment tab positioned relative to the paragraph indent or page margin. + PositionedTab(PositionedTab), } /// Block-level content elements. diff --git a/crates/office2pdf/src/ir/elements_tests.rs b/crates/office2pdf/src/ir/elements_tests.rs index 96ba0440..0c5693c5 100644 --- a/crates/office2pdf/src/ir/elements_tests.rs +++ b/crates/office2pdf/src/ir/elements_tests.rs @@ -187,6 +187,7 @@ fn test_paragraph_with_runs() { #[test] fn test_header_footer_with_text() { let hf = HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![HFInline::Run(Run { @@ -195,6 +196,8 @@ fn test_header_footer_with_text() { href: None, footnote: None, })], + border: None, + frame: None, }], }; assert_eq!(hf.paragraphs.len(), 1); @@ -208,6 +211,7 @@ fn test_header_footer_with_text() { #[test] fn test_header_footer_with_page_number() { let hf = HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![ @@ -219,6 +223,8 @@ fn test_header_footer_with_page_number() { }), HFInline::PageNumber, ], + border: None, + frame: None, }], }; assert_eq!(hf.paragraphs[0].elements.len(), 2); diff --git a/crates/office2pdf/src/lib_render_tests.rs b/crates/office2pdf/src/lib_render_tests.rs index 5407f3f5..ca17a088 100644 --- a/crates/office2pdf/src/lib_render_tests.rs +++ b/crates/office2pdf/src/lib_render_tests.rs @@ -534,6 +534,7 @@ fn test_render_document_with_header() { }], })], header: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![HFInline::Run(Run { @@ -542,6 +543,8 @@ fn test_render_document_with_header() { href: None, footnote: None, })], + border: None, + frame: None, }], }), footer: None, @@ -572,6 +575,7 @@ fn test_render_document_with_page_number_footer() { })], header: None, footer: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![ @@ -583,6 +587,8 @@ fn test_render_document_with_page_number_footer() { }), HFInline::PageNumber, ], + border: None, + frame: None, }], }), columns: None, diff --git a/crates/office2pdf/src/parser/docx_sections.rs b/crates/office2pdf/src/parser/docx_sections.rs index 676fb02f..b92bfbda 100644 --- a/crates/office2pdf/src/parser/docx_sections.rs +++ b/crates/office2pdf/src/parser/docx_sections.rs @@ -3,8 +3,10 @@ use std::io::{Cursor, Read, Seek}; use crate::error::ConvertWarning; use crate::ir::{ - Block, ColumnLayout, FlowPage, HFInline, HeaderFooter, HeaderFooterParagraph, Margins, - PageSize, Run, TextDirection, TextStyle, + Block, BorderLineStyle, BorderSide, CellBorder, Color, ColumnLayout, FlowPage, FrameAnchor, + HFInline, HeaderFooter, HeaderFooterFrame, HeaderFooterParagraph, Margins, PageSize, + PositionedTab, PositionedTabAlignment, PositionedTabRelativeTo, Run, TabLeader, TextDirection, + TextStyle, }; use super::contexts::WrapContext; @@ -15,6 +17,7 @@ use super::{ merge_paragraph_style, read_zip_text, }; use crate::parser::units::twips_to_pt; +use crate::parser::xml_util::parse_hex_color; /// Parsed header/footer assets addressed by relationship ID. #[derive(Default)] @@ -418,12 +421,21 @@ pub(super) fn build_flow_page_from_section( }); } + let mut header = extract_docx_header(section_prop, header_footer_assets); + if let Some(header) = &mut header { + header.distance_from_edge = Some(twips_to_pt(section_prop.page_margin.header)); + } + let mut footer = extract_docx_footer(section_prop, header_footer_assets); + if let Some(footer) = &mut footer { + footer.distance_from_edge = Some(twips_to_pt(section_prop.page_margin.footer)); + } + FlowPage { size, margins, content, - header: extract_docx_header(section_prop, header_footer_assets), - footer: extract_docx_footer(section_prop, header_footer_assets), + header, + footer, columns: column_layout .or_else(|| extract_column_layout_from_section_property(section_prop)), } @@ -454,7 +466,10 @@ fn convert_docx_header( if paragraphs.is_empty() { return None; } - Some(HeaderFooter { paragraphs }) + Some(HeaderFooter { + paragraphs, + distance_from_edge: None, + }) } fn convert_docx_footer( @@ -483,7 +498,10 @@ fn convert_docx_footer( if paragraphs.is_empty() { return None; } - Some(HeaderFooter { paragraphs }) + Some(HeaderFooter { + paragraphs, + distance_from_edge: None, + }) } /// Extract the header for a section, preferring the default variant and falling back to @@ -634,7 +652,87 @@ fn convert_hf_paragraph( } } - HeaderFooterParagraph { style, elements } + HeaderFooterParagraph { + style, + elements, + border: extract_hf_paragraph_border(¶graph.property), + frame: extract_hf_frame(¶graph.property), + } +} + +fn extract_hf_paragraph_border(property: &docx_rs::ParagraphProperty) -> Option { + let borders = serde_json::to_value(property.borders.as_ref()?).ok()?; + let extract_side = |key: &str| -> Option { + let side = borders.get(key)?.as_object()?; + let border_type = side + .get("borderType") + .or_else(|| side.get("val"))? + .as_str()?; + if matches!(border_type, "none" | "nil") { + return None; + } + let width = side.get("size")?.as_f64()? / 8.0; + let color = side + .get("color") + .and_then(serde_json::Value::as_str) + .filter(|value| *value != "auto") + .and_then(parse_hex_color) + .unwrap_or_else(Color::black); + let style = match border_type { + "dashed" | "dashSmallGap" => BorderLineStyle::Dashed, + "dotted" => BorderLineStyle::Dotted, + "dashDotStroked" | "dotDash" => BorderLineStyle::DashDot, + "dotDotDash" => BorderLineStyle::DashDotDot, + "double" + | "thinThickSmallGap" + | "thickThinSmallGap" + | "thinThickMediumGap" + | "thickThinMediumGap" + | "thinThickLargeGap" + | "thickThinLargeGap" + | "thinThickThinSmallGap" + | "thinThickThinMediumGap" + | "thinThickThinLargeGap" + | "triple" => BorderLineStyle::Double, + _ => BorderLineStyle::Solid, + }; + Some(BorderSide { + width, + color, + style, + }) + }; + let border = CellBorder { + top: extract_side("top"), + bottom: extract_side("bottom"), + left: extract_side("left"), + right: extract_side("right"), + }; + (border.top.is_some() + || border.bottom.is_some() + || border.left.is_some() + || border.right.is_some()) + .then_some(border) +} + +fn extract_hf_frame(property: &docx_rs::ParagraphProperty) -> Option { + let frame = property.frame_property.as_ref()?; + Some(HeaderFooterFrame { + x: frame.x.map(twips_to_pt), + y: frame.y.map(twips_to_pt), + width: frame.w.map(|value| twips_to_pt(value as i32)), + height: frame.h.map(|value| twips_to_pt(value as i32)), + horizontal_anchor: frame_anchor(frame.h_anchor.as_deref()), + vertical_anchor: frame_anchor(frame.v_anchor.as_deref()), + }) +} + +fn frame_anchor(value: Option<&str>) -> FrameAnchor { + match value { + Some("page") => FrameAnchor::Page, + Some("margin") => FrameAnchor::Margin, + _ => FrameAnchor::Text, + } } fn append_simple_fields( @@ -728,6 +826,28 @@ fn extract_hf_run_elements( footnote: None, })); } + docx_rs::RunChild::PTab(tab) if !in_field => { + let alignment = match tab.alignment { + docx_rs::PositionalTabAlignmentType::Center => PositionedTabAlignment::Center, + docx_rs::PositionalTabAlignmentType::Right => PositionedTabAlignment::Right, + docx_rs::PositionalTabAlignmentType::Left => PositionedTabAlignment::Left, + }; + let relative_to = match tab.relative_to { + docx_rs::PositionalTabRelativeTo::Indent => PositionedTabRelativeTo::Indent, + docx_rs::PositionalTabRelativeTo::Margin => PositionedTabRelativeTo::Margin, + }; + let leader = match tab.leader { + docx_rs::TabLeaderType::Dot => TabLeader::Dot, + docx_rs::TabLeaderType::Hyphen => TabLeader::Hyphen, + docx_rs::TabLeaderType::Underscore => TabLeader::Underscore, + _ => TabLeader::None, + }; + elements.push(HFInline::PositionedTab(PositionedTab { + alignment, + relative_to, + leader, + })); + } _ => {} } } diff --git a/crates/office2pdf/src/parser/xlsx_hf.rs b/crates/office2pdf/src/parser/xlsx_hf.rs index aac420b7..6d24ce42 100644 --- a/crates/office2pdf/src/parser/xlsx_hf.rs +++ b/crates/office2pdf/src/parser/xlsx_hf.rs @@ -98,6 +98,8 @@ pub(super) fn parse_hf_format_string(format_str: &str) -> Option { ..ParagraphStyle::default() }, elements, + border: None, + frame: None, }); } } @@ -105,7 +107,10 @@ pub(super) fn parse_hf_format_string(format_str: &str) -> Option { if paragraphs.is_empty() { None } else { - Some(HeaderFooter { paragraphs }) + Some(HeaderFooter { + paragraphs, + distance_from_edge: None, + }) } } diff --git a/crates/office2pdf/src/render/font_subst.rs b/crates/office2pdf/src/render/font_subst.rs index 979c669f..67b8ef36 100644 --- a/crates/office2pdf/src/render/font_subst.rs +++ b/crates/office2pdf/src/render/font_subst.rs @@ -357,7 +357,10 @@ fn visit_header_footer_fonts( .map(str::trim) .filter(|f| !f.is_empty()) .is_none_or(&mut *visitor), - HFInline::Image(_) | HFInline::PageNumber | HFInline::TotalPages => true, + HFInline::Image(_) + | HFInline::PageNumber + | HFInline::TotalPages + | HFInline::PositionedTab(_) => true, }) }) } diff --git a/crates/office2pdf/src/render/typst_gen.rs b/crates/office2pdf/src/render/typst_gen.rs index 0dd5e44f..45cecbdb 100644 --- a/crates/office2pdf/src/render/typst_gen.rs +++ b/crates/office2pdf/src/render/typst_gen.rs @@ -8,9 +8,10 @@ use crate::error::ConvertError; use crate::ir::{ Alignment, ArrowHead, Block, BorderLineStyle, BorderSide, CellBorder, CellVerticalAlign, Chart, ChartType, Color, ColumnLayout, Document, FixedElement, FixedElementKind, FixedPage, - FloatingImage, FloatingShape, FloatingTextBox, FlowPage, GradientFill, HFInline, HeaderFooter, - ImageCrop, ImageData, ImageFormat, Insets, LineSpacing, List, ListKind, Margins, MathEquation, - Metadata, Page, PageSize, Paragraph, ParagraphStyle, Run, Shadow, Shape, ShapeKind, SheetPage, + FloatingImage, FloatingShape, FloatingTextBox, FlowPage, FrameAnchor, GradientFill, HFInline, + HeaderFooter, HeaderFooterFrame, ImageCrop, ImageData, ImageFormat, Insets, LineSpacing, List, + ListKind, Margins, MathEquation, Metadata, Page, PageSize, Paragraph, ParagraphStyle, + PositionedTabAlignment, PositionedTabRelativeTo, Run, Shadow, Shape, ShapeKind, SheetPage, SmartArt, TabAlignment, TabLeader, TabStop, Table, TableCell, TableRow, TextBoxData, TextBoxVerticalAlign, TextDirection, TextStyle, VerticalTextAlign, WrapMode, }; @@ -761,35 +762,164 @@ fn write_flow_page_setup(out: &mut String, page: &FlowPage, size: &PageSize, ctx format_f64(page.margins.right), ); - if let Some(header) = &page.header { + if let Some(header) = &page.header + && hf_has_flow_content(header) + { if hf_needs_context(header) { out.push_str(", header: context ["); } else { out.push_str(", header: ["); } - generate_hf_content(out, header, ctx); + generate_flow_hf_content(out, header, ctx); out.push(']'); } - if let Some(footer) = &page.footer { - if hf_needs_stack_offset(footer) { + if let Some(footer) = &page.footer + && hf_has_flow_content(footer) + { + let edge_offset = footer + .distance_from_edge + .map(|distance| (page.margins.bottom - distance).max(0.0)) + .unwrap_or(0.0); + if hf_needs_stack_offset(footer) || edge_offset > 0.0 { out.push_str(", footer: context { let footer_content = block(width: 100%)["); - generate_hf_content(out, footer, ctx); - out.push_str("]; move(dy: -measure(footer_content).height / 2)[#footer_content] }"); + generate_flow_hf_content(out, footer, ctx); + out.push_str("]; move(dy: "); + if hf_needs_stack_offset(footer) { + out.push_str("-measure(footer_content).height / 2"); + if edge_offset > 0.0 { + let _ = write!(out, " - {}pt", format_f64(edge_offset)); + } + } else { + let _ = write!(out, "-{}pt", format_f64(edge_offset)); + } + out.push_str(")[#footer_content] }"); } else if hf_needs_context(footer) { out.push_str(", footer: context ["); - generate_hf_content(out, footer, ctx); + generate_flow_hf_content(out, footer, ctx); out.push(']'); } else { out.push_str(", footer: ["); - generate_hf_content(out, footer, ctx); + generate_flow_hf_content(out, footer, ctx); out.push(']'); } } + if page + .header + .iter() + .chain(page.footer.iter()) + .any(hf_has_page_anchored_frames) + { + out.push_str(", foreground: ["); + if let Some(header) = &page.header { + generate_page_anchored_hf_frames(out, header, size.width, page.margins.right, ctx); + } + if let Some(footer) = &page.footer { + generate_page_anchored_hf_frames(out, footer, size.width, page.margins.right, ctx); + } + out.push(']'); + } + out.push_str(")\n"); } +fn is_page_anchored_frame(frame: &HeaderFooterFrame) -> bool { + frame.horizontal_anchor == FrameAnchor::Page && frame.vertical_anchor == FrameAnchor::Page +} + +fn hf_has_flow_content(header_footer: &HeaderFooter) -> bool { + header_footer + .paragraphs + .iter() + .any(hf_paragraph_has_flow_content) +} + +fn hf_paragraph_has_content(paragraph: &crate::ir::HeaderFooterParagraph) -> bool { + !paragraph.elements.is_empty() || paragraph.border.is_some() +} + +fn hf_paragraph_has_flow_content(paragraph: &crate::ir::HeaderFooterParagraph) -> bool { + hf_paragraph_has_content(paragraph) + && paragraph + .frame + .as_ref() + .is_none_or(|frame| !is_page_anchored_frame(frame)) +} + +fn hf_has_page_anchored_frames(header_footer: &HeaderFooter) -> bool { + header_footer + .paragraphs + .iter() + .any(|paragraph| paragraph.frame.as_ref().is_some_and(is_page_anchored_frame)) +} + +fn generate_flow_hf_content(out: &mut String, hf: &HeaderFooter, ctx: &mut GenCtx) { + let mut is_first: bool = true; + for paragraph in &hf.paragraphs { + if paragraph.frame.as_ref().is_some_and(is_page_anchored_frame) + || !hf_paragraph_has_content(paragraph) + { + continue; + } + if !is_first { + out.push_str("\\\n"); + } + generate_hf_styled_paragraph(out, paragraph, ctx); + is_first = false; + } +} + +fn generate_page_anchored_hf_frames( + out: &mut String, + hf: &HeaderFooter, + page_width: f64, + right_margin: f64, + ctx: &mut GenCtx, +) { + let mut index: usize = 0; + while index < hf.paragraphs.len() { + let Some(frame) = hf.paragraphs[index].frame.as_ref() else { + index += 1; + continue; + }; + if !is_page_anchored_frame(frame) { + index += 1; + continue; + } + let mut end: usize = index + 1; + while end < hf.paragraphs.len() && hf.paragraphs[end].frame.as_ref() == Some(frame) { + end += 1; + } + let x = frame.x.unwrap_or(0.0); + let y = frame.y.unwrap_or(0.0); + let _ = write!( + out, + "#place(top + left, dx: {}pt, dy: {}pt)[#block(", + format_f64(x), + format_f64(y) + ); + if let Some(width) = frame.width { + let _ = write!(out, "width: {}pt", format_f64(width)); + } else { + let width = (page_width - x - right_margin).max(0.0); + let _ = write!(out, "width: {}pt", format_f64(width)); + } + out.push_str(")[#stack(dir: ttb, spacing: 4pt"); + for paragraph in &hf.paragraphs[index..end] { + out.push_str(", ["); + if hf_paragraph_has_content(paragraph) { + generate_hf_styled_paragraph(out, paragraph, ctx); + } else { + out.push_str("#box(height: 12pt)"); + } + out.push(']'); + } + out.push_str(")]]"); + index = end; + } +} + /// Write the full page setup for a SheetPage, including optional header/footer. fn write_table_page_setup(out: &mut String, page: &SheetPage, size: &PageSize, ctx: &mut GenCtx) { if page.header.is_none() && page.footer.is_none() { @@ -847,10 +977,15 @@ fn hf_needs_context(hf: &HeaderFooter) -> bool { } fn hf_needs_stack_offset(hf: &HeaderFooter) -> bool { - hf.paragraphs.len() > 1 + hf.paragraphs + .iter() + .filter(|paragraph| hf_paragraph_has_flow_content(paragraph)) + .count() + > 1 || hf .paragraphs .iter() + .filter(|paragraph| hf_paragraph_has_flow_content(paragraph)) .flat_map(|paragraph| ¶graph.elements) .any(|element| matches!(element, HFInline::Image(_))) } @@ -861,40 +996,112 @@ fn generate_hf_content(out: &mut String, hf: &HeaderFooter, ctx: &mut GenCtx) { if i > 0 { out.push_str("\\\n"); } - // Apply paragraph alignment if set - if let Some(align) = para.style.alignment { - let align_str = match align { - Alignment::Left => "left", - Alignment::Center => "center", - Alignment::Right => "right", - Alignment::Justify => "left", - }; - let _ = write!(out, "#align({align_str})["); - } - if para.style.direction == Some(TextDirection::Rtl) { - out.push_str("#text(dir: rtl)["); - } - for elem in ¶.elements { - match elem { - HFInline::Run(run) => { - generate_run(out, run); - } - HFInline::Image(image) => { - generate_image(out, image, ctx); - } - HFInline::PageNumber => { - out.push_str("#counter(page).display()"); - } - HFInline::TotalPages => { - out.push_str("#counter(page).final().first()"); - } - } + generate_hf_styled_paragraph(out, para, ctx); + } +} + +fn generate_hf_styled_paragraph( + out: &mut String, + paragraph: &crate::ir::HeaderFooterParagraph, + ctx: &mut GenCtx, +) { + if let Some(align) = paragraph.style.alignment { + let align_str = match align { + Alignment::Left => "left", + Alignment::Center => "center", + Alignment::Right => "right", + Alignment::Justify => "left", + }; + let _ = write!(out, "#align({align_str})["); + } + if paragraph.style.direction == Some(TextDirection::Rtl) { + out.push_str("#text(dir: rtl)["); + } + generate_hf_paragraph(out, paragraph, ctx); + if paragraph.style.direction == Some(TextDirection::Rtl) { + out.push(']'); + } + if paragraph.style.alignment.is_some() { + out.push(']'); + } +} + +fn generate_hf_paragraph( + out: &mut String, + paragraph: &crate::ir::HeaderFooterParagraph, + ctx: &mut GenCtx, +) { + let right_tab = paragraph.elements.iter().position(|element| { + matches!( + element, + HFInline::PositionedTab(tab) + if tab.alignment == PositionedTabAlignment::Right + && tab.relative_to == PositionedTabRelativeTo::Margin + ) + }); + let has_top_border = paragraph + .border + .as_ref() + .and_then(|border| border.top.as_ref()); + + if let Some(border) = has_top_border { + out.push_str("#stack(dir: ttb, spacing: 0.5pt, "); + write_hf_border_line(out, border, border.style == BorderLineStyle::Double); + if border.style == BorderLineStyle::Double { + out.push_str(", "); + write_hf_border_line(out, border, false); } - if para.style.direction == Some(TextDirection::Rtl) { - out.push(']'); + out.push_str(", ["); + } + + if let Some(index) = right_tab { + out.push_str("#grid(columns: (1fr, auto), ["); + generate_hf_elements(out, ¶graph.elements[..index], ctx); + out.push_str("], ["); + generate_hf_elements(out, ¶graph.elements[index + 1..], ctx); + out.push_str("])"); + } else { + generate_hf_elements(out, ¶graph.elements, ctx); + } + + if has_top_border.is_some() { + out.push_str("])"); + } +} + +fn write_hf_border_line(out: &mut String, border: &BorderSide, is_primary_double: bool) { + let width = if is_primary_double { + border.width * 0.67 + } else if border.style == BorderLineStyle::Double { + border.width * 0.17 + } else { + border.width + }; + let dash = border_line_style_to_typst(border.style); + let _ = write!( + out, + "block(height: {}pt)[#line(length: 100%, stroke: (paint: rgb({}, {}, {}), thickness: {}pt, dash: \"{}\"))]", + format_f64(width), + border.color.r, + border.color.g, + border.color.b, + format_f64(width), + if border.style == BorderLineStyle::Double { + "solid" + } else { + dash } - if para.style.alignment.is_some() { - out.push(']'); + ); +} + +fn generate_hf_elements(out: &mut String, elements: &[HFInline], ctx: &mut GenCtx) { + for element in elements { + match element { + HFInline::Run(run) => generate_run(out, run), + HFInline::Image(image) => generate_image(out, image, ctx), + HFInline::PageNumber => out.push_str("#counter(page).display()"), + HFInline::TotalPages => out.push_str("#counter(page).final().first()"), + HFInline::PositionedTab(_) => out.push_str("#h(1em)"), } } } diff --git a/crates/office2pdf/src/render/typst_gen_page_misc_tests.rs b/crates/office2pdf/src/render/typst_gen_page_misc_tests.rs index 241b1d29..cc68235a 100644 --- a/crates/office2pdf/src/render/typst_gen_page_misc_tests.rs +++ b/crates/office2pdf/src/render/typst_gen_page_misc_tests.rs @@ -9,6 +9,7 @@ fn test_generate_flow_page_with_text_header() { margins: Margins::default(), content: vec![make_paragraph("Body text")], header: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![HFInline::Run(Run { @@ -17,6 +18,8 @@ fn test_generate_flow_page_with_text_header() { href: None, footnote: None, })], + border: None, + frame: None, }], }), footer: None, @@ -37,6 +40,7 @@ fn test_generate_flow_page_with_page_number_footer() { content: vec![make_paragraph("Body text")], header: None, footer: Some(HeaderFooter { + distance_from_edge: Some(35.4), paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![ @@ -48,6 +52,8 @@ fn test_generate_flow_page_with_page_number_footer() { }), HFInline::PageNumber, ], + border: None, + frame: None, }], }), columns: None, @@ -56,6 +62,110 @@ fn test_generate_flow_page_with_page_number_footer() { assert!(output.source.contains("footer:")); assert!(output.source.contains("counter(page).display()")); assert!(output.source.contains("Page ")); + assert!(output.source.contains("move(dy: -36.6pt)")); +} + +#[test] +fn test_generate_footer_with_compound_border_and_right_positioned_tab() { + use crate::ir::{ + BorderSide, CellBorder, HFInline, HeaderFooter, HeaderFooterParagraph, PositionedTab, + PositionedTabAlignment, PositionedTabRelativeTo, + }; + + let doc = make_doc(vec![Page::Flow(FlowPage { + size: PageSize::default(), + margins: Margins::default(), + content: vec![make_paragraph("Body")], + header: None, + footer: Some(HeaderFooter { + distance_from_edge: None, + paragraphs: vec![HeaderFooterParagraph { + style: ParagraphStyle::default(), + elements: vec![ + HFInline::Run(Run { + text: "Left".to_string(), + style: TextStyle::default(), + href: None, + footnote: None, + }), + HFInline::PositionedTab(PositionedTab { + alignment: PositionedTabAlignment::Right, + relative_to: PositionedTabRelativeTo::Margin, + leader: TabLeader::None, + }), + HFInline::Run(Run { + text: "Page ".to_string(), + style: TextStyle::default(), + href: None, + footnote: None, + }), + HFInline::PageNumber, + ], + border: Some(CellBorder { + top: Some(BorderSide { + width: 3.0, + color: Color::new(0x62, 0x24, 0x23), + style: BorderLineStyle::Double, + }), + bottom: None, + left: None, + right: None, + }), + frame: None, + }], + }), + columns: None, + })]); + + let output = generate_typst(&doc).unwrap(); + assert!(output.source.contains("#grid(columns: (1fr, auto)")); + assert!(output.source.contains("rgb(98, 36, 35)")); + assert_eq!(output.source.matches("line(length: 100%").count(), 2); +} + +#[test] +fn test_generate_page_anchored_footer_frame_in_foreground() { + use crate::ir::{ + FrameAnchor, HFInline, HeaderFooter, HeaderFooterFrame, HeaderFooterParagraph, + }; + + let doc = make_doc(vec![Page::Flow(FlowPage { + size: PageSize::default(), + margins: Margins::default(), + content: vec![make_paragraph("Body")], + header: None, + footer: Some(HeaderFooter { + distance_from_edge: None, + paragraphs: vec![HeaderFooterParagraph { + style: ParagraphStyle::default(), + elements: vec![HFInline::Run(Run { + text: "Framed footer".to_string(), + style: TextStyle::default(), + href: None, + footnote: None, + })], + border: None, + frame: Some(HeaderFooterFrame { + x: Some(71.8), + y: Some(198.5), + width: None, + height: None, + horizontal_anchor: FrameAnchor::Page, + vertical_anchor: FrameAnchor::Page, + }), + }], + }), + columns: None, + })]); + + let output = generate_typst(&doc).unwrap(); + assert!(output.source.contains("foreground: [")); + assert!( + output + .source + .contains("#place(top + left, dx: 71.8pt, dy: 198.5pt)") + ); + assert!(!output.source.contains("footer:")); } #[test] @@ -67,6 +177,7 @@ fn test_generate_flow_page_with_header_and_footer() { margins: Margins::default(), content: vec![make_paragraph("Body")], header: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![HFInline::Run(Run { @@ -75,12 +186,17 @@ fn test_generate_flow_page_with_header_and_footer() { href: None, footnote: None, })], + border: None, + frame: None, }], }), footer: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle::default(), elements: vec![HFInline::PageNumber], + border: None, + frame: None, }], }), columns: None, @@ -368,6 +484,7 @@ fn test_table_page_with_header() { margins: Margins::default(), table: make_simple_table(vec![vec!["A"]]), header: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle { alignment: Some(Alignment::Center), @@ -379,6 +496,8 @@ fn test_table_page_with_header() { href: None, footnote: None, })], + border: None, + frame: None, }], }), footer: None, @@ -399,6 +518,7 @@ fn test_table_page_with_page_number_footer() { table: make_simple_table(vec![vec!["A"]]), header: None, footer: Some(HeaderFooter { + distance_from_edge: None, paragraphs: vec![HeaderFooterParagraph { style: ParagraphStyle { alignment: Some(Alignment::Center), @@ -420,6 +540,8 @@ fn test_table_page_with_page_number_footer() { }), HFInline::TotalPages, ], + border: None, + frame: None, }], }), charts: vec![], diff --git a/crates/office2pdf/tests/docx_fixtures.rs b/crates/office2pdf/tests/docx_fixtures.rs index 73488e05..5b46be67 100644 --- a/crates/office2pdf/tests/docx_fixtures.rs +++ b/crates/office2pdf/tests/docx_fixtures.rs @@ -11,7 +11,8 @@ use std::path::PathBuf; use office2pdf::config::ConvertOptions; use office2pdf::ir::{ - ArrowHead, Block, Color, FlowPage, HFInline, ListKind, Page, Paragraph, Run, ShapeKind, + ArrowHead, Block, BorderLineStyle, Color, FlowPage, FrameAnchor, HFInline, ListKind, Page, + Paragraph, PositionedTabAlignment, PositionedTabRelativeTo, Run, ShapeKind, TextBoxVerticalAlign, }; use office2pdf::parser::Parser; @@ -846,6 +847,8 @@ docx_fixture_tests!(fancy_foot, "FancyFoot.docx"); #[test] fn structure_fancy_foot_preserves_text_and_simple_page_field() { let pages = flow_pages("FancyFoot.docx"); + let footer = pages[0].footer.as_ref().expect("default footer"); + let first_paragraph = &footer.paragraphs[0]; let elements = footer_elements(&pages); let text = footer_text(&pages); @@ -863,6 +866,21 @@ fn structure_fancy_foot_preserves_text_and_simple_page_field() { .any(|element| matches!(element, HFInline::PageNumber)), "the real-world fldSimple PAGE field should survive parsing" ); + let top_border = first_paragraph + .border + .as_ref() + .and_then(|border| border.top.as_ref()) + .expect("the compound top border should survive parsing"); + assert_eq!(top_border.style, BorderLineStyle::Double); + assert!((top_border.width - 3.0).abs() < f64::EPSILON); + assert_eq!(top_border.color, Color::new(0x62, 0x24, 0x23)); + assert!(first_paragraph.elements.iter().any(|element| matches!( + element, + HFInline::PositionedTab(tab) + if tab.alignment == PositionedTabAlignment::Right + && tab.relative_to == PositionedTabRelativeTo::Margin + ))); + assert!((footer.distance_from_edge.expect("footer distance") - 35.4).abs() < 0.01); } docx_fixture_tests!(field_codes, "FieldCodes.docx"); docx_fixture_tests!(header_footer_unicode, "HeaderFooterUnicode.docx"); @@ -1062,6 +1080,40 @@ docx_fixture_tests!(tdf111550, "libreoffice/tdf111550.docx"); docx_fixture_tests!(tdf111964, "libreoffice/tdf111964.docx"); docx_fixture_tests!(tdf124670, "libreoffice/tdf124670.docx"); docx_fixture_tests!(tdf129659, "libreoffice/tdf129659.docx"); +docx_fixture_tests!( + tdf159207_footer_frame_border, + "libreoffice/tdf159207_footerFramePrBorder.docx" +); + +#[test] +fn structure_tdf159207_preserves_page_anchored_footer_frame_position() { + let pages = flow_pages("libreoffice/tdf159207_footerFramePrBorder.docx"); + let footer = pages[0].footer.as_ref().expect("default footer"); + let framed_paragraphs = footer + .paragraphs + .iter() + .filter_map(|paragraph| paragraph.frame.as_ref()) + .collect::>(); + + assert_eq!(framed_paragraphs.len(), 6); + for frame in framed_paragraphs { + assert_eq!(frame.horizontal_anchor, FrameAnchor::Page); + assert_eq!(frame.vertical_anchor, FrameAnchor::Page); + assert!((frame.x.expect("absolute x") - 71.8).abs() < 0.01); + assert!((frame.y.expect("absolute y") - 198.5).abs() < 0.01); + } +} + +#[test] +fn conversion_tdf159207_renders_positioned_footer_frame() { + let result = office2pdf::convert(fixture_path( + "libreoffice/tdf159207_footerFramePrBorder.docx", + )) + .expect("the positioned footer frame should render"); + + assert!(result.pdf.starts_with(b"%PDF")); + common::validate_pdf_with_qpdf(&result.pdf); +} docx_fixture_tests!(table_rtl, "libreoffice/table-rtl.docx"); docx_fixture_tests!(wpg_only, "libreoffice/wpg-only.docx");