using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using A = DocumentFormat.OpenXml.Drawing; using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing; using PIC = DocumentFormat.OpenXml.Drawing.Pictures; namespace MiniMaxAIDocx.Core.Samples; /// /// Comprehensive reference for OpenXML headers, footers, and page numbers. /// /// Architecture: /// - Headers/footers live in separate HeaderPart/FooterPart containers. /// - They are linked to sections via HeaderReference/FooterReference in SectionProperties. /// - Each reference has a Type: Default, First, Even. /// - The relationship ID (r:id) connects the reference to the part. /// /// XML structure in SectionProperties: /// /// /// /// /// /// /// /// Header/Footer XML (in separate part): /// (or ) /// /// ... /// Header text /// /// /// /// Page number fields use complex field codes: /// PAGE — current page number /// NUMPAGES — total page count /// public static class HeaderFooterSamples { // ────────────────────────────────────────────────────────────── // 1. AddSimpleHeader — basic text header // ────────────────────────────────────────────────────────────── /// /// Adds a simple text header to the default header slot. /// /// Steps: /// 1. Create a HeaderPart on the MainDocumentPart /// 2. Set its Header content (must contain at least one Paragraph) /// 3. Get the relationship ID /// 4. Add HeaderReference to SectionProperties with type="default" /// /// XML in header part: /// /// /// /// /// /// My Document Header /// /// /// /// /// XML in sectPr: /// /// public static void AddSimpleHeader(MainDocumentPart mainPart, SectionProperties sectPr, string text) { var headerPart = mainPart.AddNewPart(); headerPart.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Right }), new Run( new RunProperties( new Color { Val = "808080" }, new FontSize { Val = "18" }), // 9pt (half-points) new Text(text) { Space = SpaceProcessingModeValues.Preserve }))); headerPart.Header.Save(); var headerRefId = mainPart.GetIdOfPart(headerPart); sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Default, Id = headerRefId }); } // ────────────────────────────────────────────────────────────── // 2. AddSimpleFooter — basic text footer // ────────────────────────────────────────────────────────────── /// /// Adds a simple text footer to the default footer slot. /// /// XML in footer part: /// /// /// /// Confidential /// /// /// /// XML in sectPr: /// /// public static void AddSimpleFooter(MainDocumentPart mainPart, SectionProperties sectPr, string text) { var footerPart = mainPart.AddNewPart(); footerPart.Footer = new Footer( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Center }), new Run( new RunProperties( new Color { Val = "808080" }, new FontSize { Val = "18" }), new Text(text) { Space = SpaceProcessingModeValues.Preserve }))); footerPart.Footer.Save(); var footerRefId = mainPart.GetIdOfPart(footerPart); sectPr.Append(new FooterReference { Type = HeaderFooterValues.Default, Id = footerRefId }); } // ────────────────────────────────────────────────────────────── // 3. AddPageNumberFooter — centered page number // ────────────────────────────────────────────────────────────── /// /// Adds a centered page number footer using the PAGE field code. /// /// Field code pattern (3 runs): /// Run 1: FieldChar Begin /// Run 2: FieldCode " PAGE " /// Run 3: FieldChar End /// /// XML: /// /// /// /// /// PAGE /// /// /// /// /// GOTCHA: FieldCode text MUST have leading/trailing spaces: " PAGE ", not "PAGE". /// GOTCHA: Use Space = SpaceProcessingModeValues.Preserve on FieldCode to keep spaces. /// public static void AddPageNumberFooter(MainDocumentPart mainPart, SectionProperties sectPr) { var footerPart = mainPart.AddNewPart(); var paragraph = new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Center })); // PAGE field: Begin → InstrText → End paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin })); paragraph.Append(new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve })); paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End })); footerPart.Footer = new Footer(paragraph); footerPart.Footer.Save(); var footerRefId = mainPart.GetIdOfPart(footerPart); sectPr.Append(new FooterReference { Type = HeaderFooterValues.Default, Id = footerRefId }); } // ────────────────────────────────────────────────────────────── // 4. AddPageXofYFooter — "Page X of Y" // ────────────────────────────────────────────────────────────── /// /// Adds a footer with "Page X of Y" format using PAGE and NUMPAGES field codes. /// /// XML: /// /// /// /// Page /// /// PAGE /// /// of /// /// NUMPAGES /// /// /// /// public static void AddPageXofYFooter(MainDocumentPart mainPart, SectionProperties sectPr) { var footerPart = mainPart.AddNewPart(); var paragraph = new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Center })); // "Page " paragraph.Append(new Run(new Text("Page ") { Space = SpaceProcessingModeValues.Preserve })); // PAGE field paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin })); paragraph.Append(new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve })); paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End })); // " of " paragraph.Append(new Run(new Text(" of ") { Space = SpaceProcessingModeValues.Preserve })); // NUMPAGES field paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin })); paragraph.Append(new Run(new FieldCode(" NUMPAGES ") { Space = SpaceProcessingModeValues.Preserve })); paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End })); footerPart.Footer = new Footer(paragraph); footerPart.Footer.Save(); var footerRefId = mainPart.GetIdOfPart(footerPart); sectPr.Append(new FooterReference { Type = HeaderFooterValues.Default, Id = footerRefId }); } // ────────────────────────────────────────────────────────────── // 5. AddDifferentFirstPageHeader — TitlePage element // ────────────────────────────────────────────────────────────── /// /// Adds a different header for the first page vs. subsequent pages. /// /// Requires: /// 1. in SectionProperties to enable first-page header/footer /// 2. HeaderReference with Type="first" for the first page header /// 3. HeaderReference with Type="default" for subsequent pages /// /// XML in sectPr: /// /// /// /// /// /// /// GOTCHA: Without , the "first" type header is completely ignored. /// GOTCHA: If you want a blank first-page header, you still need a HeaderPart /// with an empty Paragraph — just don't add text to it. /// public static void AddDifferentFirstPageHeader(MainDocumentPart mainPart, SectionProperties sectPr) { // First page header: e.g., cover page with large title var firstHeaderPart = mainPart.AddNewPart(); firstHeaderPart.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Center }), new Run( new RunProperties( new Bold(), new FontSize { Val = "32" }), // 16pt new Text("COMPANY CONFIDENTIAL")))); firstHeaderPart.Header.Save(); // Default header for subsequent pages var defaultHeaderPart = mainPart.AddNewPart(); defaultHeaderPart.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Right }), new Run( new RunProperties( new FontSize { Val = "18" }), // 9pt new Text("Internal Document")))); defaultHeaderPart.Header.Save(); // Link both headers to section sectPr.Append(new HeaderReference { Type = HeaderFooterValues.First, Id = mainPart.GetIdOfPart(firstHeaderPart) }); sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Default, Id = mainPart.GetIdOfPart(defaultHeaderPart) }); // CRITICAL: Enable first page header/footer sectPr.Append(new TitlePage()); } // ────────────────────────────────────────────────────────────── // 6. AddEvenOddHeaders — EvenAndOddHeaders in Settings // ────────────────────────────────────────────────────────────── /// /// Creates different headers for even and odd pages (e.g., for book-style printing). /// /// Requires: /// 1. in document Settings (DocumentSettingsPart) /// 2. HeaderReference with Type="default" for odd pages /// 3. HeaderReference with Type="even" for even pages /// /// XML in settings.xml: /// /// /// /// /// XML in sectPr: /// /// /// /// /// /// GOTCHA: "default" means ODD pages when evenAndOddHeaders is enabled. /// GOTCHA: Without the Settings flag, the "even" header is ignored entirely. /// public static void AddEvenOddHeaders(MainDocumentPart mainPart, SectionProperties sectPr) { // Enable even/odd header distinction in document settings var settingsPart = mainPart.DocumentSettingsPart ?? mainPart.AddNewPart(); if (settingsPart.Settings == null) settingsPart.Settings = new Settings(); // Add EvenAndOddHeaders if not already present if (settingsPart.Settings.GetFirstChild() == null) { settingsPart.Settings.Append(new EvenAndOddHeaders()); } settingsPart.Settings.Save(); // Odd page header (Type="default" means odd when even/odd is enabled) var oddHeaderPart = mainPart.AddNewPart(); oddHeaderPart.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Right }), new Run(new Text("Chapter Title — Odd Page")))); oddHeaderPart.Header.Save(); // Even page header var evenHeaderPart = mainPart.AddNewPart(); evenHeaderPart.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Left }), new Run(new Text("Book Title — Even Page")))); evenHeaderPart.Header.Save(); // Link to section sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Default, // = odd pages Id = mainPart.GetIdOfPart(oddHeaderPart) }); sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Even, Id = mainPart.GetIdOfPart(evenHeaderPart) }); } // ────────────────────────────────────────────────────────────── // 7. AddHeaderWithLogo — image in header // ────────────────────────────────────────────────────────────── /// /// Adds a header containing an image (logo). /// /// Steps: /// 1. Create HeaderPart /// 2. Add ImagePart to the HeaderPart (NOT to MainDocumentPart) /// 3. Feed the image stream /// 4. Build Drawing element with inline image /// 5. Link HeaderPart to sectPr /// /// Image sizing uses EMU (English Metric Units): /// 914400 EMU = 1 inch /// 360000 EMU = 1 cm /// /// XML for inline image: /// /// /// /// /// /// /// /// ... /// /// ... /// /// /// /// /// /// /// GOTCHA: The ImagePart must be added to the HeaderPart, not the MainDocumentPart. /// If you add it to MainDocumentPart, the relationship ID won't resolve in the header. /// public static void AddHeaderWithLogo(MainDocumentPart mainPart, SectionProperties sectPr, string imagePath) { var headerPart = mainPart.AddNewPart(); // Add image part to the HEADER part (not main document part) var imagePart = headerPart.AddImagePart(ImagePartType.Png); using (var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) { imagePart.FeedData(stream); } var imageRelId = headerPart.GetIdOfPart(imagePart); // Image dimensions in EMU: 1 inch wide x 0.5 inch tall long widthEmu = 914400; // 1 inch long heightEmu = 457200; // 0.5 inch // Build the Drawing element with inline image var drawing = new Drawing( new DW.Inline( new DW.Extent { Cx = widthEmu, Cy = heightEmu }, new DW.EffectExtent { LeftEdge = 0, TopEdge = 0, RightEdge = 0, BottomEdge = 0 }, new DW.DocProperties { Id = 1U, Name = "Logo" }, new A.Graphic( new A.GraphicData( new PIC.Picture( new PIC.NonVisualPictureProperties( new PIC.NonVisualDrawingProperties { Id = 0U, Name = "logo.png" }, new PIC.NonVisualPictureDrawingProperties()), new PIC.BlipFill( new A.Blip { Embed = imageRelId }, new A.Stretch(new A.FillRectangle())), new PIC.ShapeProperties( new A.Transform2D( new A.Offset { X = 0, Y = 0 }, new A.Extents { Cx = widthEmu, Cy = heightEmu }), new A.PresetGeometry( new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle })) ) { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" }) ) { DistanceFromTop = 0U, DistanceFromBottom = 0U, DistanceFromLeft = 0U, DistanceFromRight = 0U }); headerPart.Header = new Header( new Paragraph(new Run(drawing))); headerPart.Header.Save(); var headerRefId = mainPart.GetIdOfPart(headerPart); sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Default, Id = headerRefId }); } // ────────────────────────────────────────────────────────────── // 8. AddTableLayoutHeader — 3-column invisible table // ────────────────────────────────────────────────────────────── /// /// Creates a header with a 3-column invisible table for precise layout: /// Left cell: Logo placeholder text /// Center cell: Document title (centered) /// Right cell: Page number (right-aligned) /// /// The table has no borders, so it's invisible but provides column alignment. /// /// XML structure: /// /// /// /// /// /// ... /// /// /// /// /// /// /// /// /// /// /// /// /// public static void AddTableLayoutHeader(MainDocumentPart mainPart, SectionProperties sectPr) { var headerPart = mainPart.AddNewPart(); // Invisible table (no borders) var table = new Table(); var tblPr = new TableProperties( new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct }, new TableBorders( new TopBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" }, new LeftBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" }, new BottomBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" }, new RightBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" }, new InsideHorizontalBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" }, new InsideVerticalBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" } ), // Fixed layout so columns don't shift new TableLayout { Type = TableLayoutValues.Fixed }); table.Append(tblPr); var grid = new TableGrid( new GridColumn { Width = "3120" }, new GridColumn { Width = "3120" }, new GridColumn { Width = "3120" }); table.Append(grid); var row = new TableRow(); // Left cell: logo/company name var leftCell = new TableCell( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Left }), new Run( new RunProperties(new Bold(), new FontSize { Val = "18" }), new Text("ACME Corp")))); row.Append(leftCell); // Center cell: document title var centerCell = new TableCell( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Center }), new Run( new RunProperties(new FontSize { Val = "18" }), new Text("Technical Report")))); row.Append(centerCell); // Right cell: page number var pageNumPara = new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Right })); pageNumPara.Append(new Run( new RunProperties(new FontSize { Val = "18" }), new Text("Page ") { Space = SpaceProcessingModeValues.Preserve })); pageNumPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin })); pageNumPara.Append(new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve })); pageNumPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End })); var rightCell = new TableCell(pageNumPara); row.Append(rightCell); table.Append(row); headerPart.Header = new Header(table); headerPart.Header.Save(); var headerRefId = mainPart.GetIdOfPart(headerPart); sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Default, Id = headerRefId }); } // ────────────────────────────────────────────────────────────── // 9. AddChineseGongWenFooter — "-X-" format, SimSun 14pt // ────────────────────────────────────────────────────────────── /// /// Adds a Chinese government document (公文) style footer: /// - Page number in "-X-" format (e.g., "- 1 -") /// - Centered at bottom /// - SimSun (宋体) font, 14pt (Chinese 四号) /// /// XML: /// /// /// /// /// /// /// /// /// - /// /// ..PAGE field.. /// /// ... /// - /// /// /// /// /// Chinese font size reference: /// 四号 = 14pt = sz val="28" (half-points) /// 小四 = 12pt = sz val="24" /// 五号 = 10.5pt = sz val="21" /// public static void AddChineseGongWenFooter(MainDocumentPart mainPart, SectionProperties sectPr) { var footerPart = mainPart.AddNewPart(); // Common run properties for the footer: SimSun 14pt (四号) // 14pt = 28 half-points RunProperties MakeGongWenRunProps() => new RunProperties( new RunFonts { Ascii = "SimSun", EastAsia = "SimSun", HighAnsi = "SimSun" }, new FontSize { Val = "28" }, new FontSizeComplexScript { Val = "28" }); var paragraph = new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Center })); // "- " prefix paragraph.Append(new Run( MakeGongWenRunProps(), new Text("- ") { Space = SpaceProcessingModeValues.Preserve })); // PAGE field with same formatting paragraph.Append(new Run( MakeGongWenRunProps(), new FieldChar { FieldCharType = FieldCharValues.Begin })); paragraph.Append(new Run( MakeGongWenRunProps(), new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve })); paragraph.Append(new Run( MakeGongWenRunProps(), new FieldChar { FieldCharType = FieldCharValues.End })); // " -" suffix paragraph.Append(new Run( MakeGongWenRunProps(), new Text(" -") { Space = SpaceProcessingModeValues.Preserve })); footerPart.Footer = new Footer(paragraph); footerPart.Footer.Save(); var footerRefId = mainPart.GetIdOfPart(footerPart); sectPr.Append(new FooterReference { Type = HeaderFooterValues.Default, Id = footerRefId }); } // ────────────────────────────────────────────────────────────── // 10. AddHeaderWithHorizontalLine — bottom border line // ────────────────────────────────────────────────────────────── /// /// Adds a header with a horizontal line (bottom border) beneath the text. /// This is a common style: header text with a line separating it from content. /// /// The line is achieved via a paragraph bottom border in the header, NOT a /// separate drawing element. /// /// XML: /// /// /// /// /// /// /// /// /// Document Header /// /// /// /// Border space attribute: space between text and border line, in points. /// Border size: in eighth-points (6 = 0.75pt). /// public static void AddHeaderWithHorizontalLine(MainDocumentPart mainPart, SectionProperties sectPr) { var headerPart = mainPart.AddNewPart(); var paragraph = new Paragraph( new ParagraphProperties( new ParagraphBorders( new BottomBorder { Val = BorderValues.Single, Size = 6, // 0.75pt line (in eighth-points) Space = 1, // 1pt spacing between text and line Color = "000000" }), new Justification { Val = JustificationValues.Center }), new Run( new RunProperties( new Bold(), new FontSize { Val = "20" }), // 10pt new Text("Document Header"))); headerPart.Header = new Header(paragraph); headerPart.Header.Save(); var headerRefId = mainPart.GetIdOfPart(headerPart); sectPr.Append(new HeaderReference { Type = HeaderFooterValues.Default, Id = headerRefId }); } // ────────────────────────────────────────────────────────────── // 11. ChangeHeaderPerSection — different headers per section // ────────────────────────────────────────────────────────────── /// /// Creates a document with multiple sections, each having its own header. /// /// In OOXML, sections are delimited by SectionProperties: /// - Inner sections: sectPr inside a Paragraph's ParagraphProperties (section break) /// - Last section: sectPr as direct child of Body /// /// Each sectPr can reference different HeaderPart/FooterPart via its own /// HeaderReference/FooterReference elements. /// /// XML structure for multi-section document: /// /// /// Section 1 content /// /// /// /// /// /// /// /// /// /// /// Section 2 content /// /// /// /// /// /// /// /// GOTCHA: A section break sectPr is placed inside a paragraph's ParagraphProperties. /// The paragraph that contains the sectPr is the LAST paragraph of that section. /// /// GOTCHA: If a section does not have its own HeaderReference, it inherits /// the header from the previous section. To have NO header in a section, /// you must explicitly link to an empty HeaderPart. /// public static void ChangeHeaderPerSection(MainDocumentPart mainPart, Body body) { // --- Create two different header parts --- // Header for Section 1 var header1Part = mainPart.AddNewPart(); header1Part.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Left }), new Run(new Text("Section 1 — Introduction")))); header1Part.Header.Save(); // Header for Section 2 var header2Part = mainPart.AddNewPart(); header2Part.Header = new Header( new Paragraph( new ParagraphProperties( new Justification { Val = JustificationValues.Left }), new Run(new Text("Section 2 — Analysis")))); header2Part.Header.Save(); // --- Section 1 content --- body.Append(new Paragraph( new Run(new Text("This is content in Section 1.")))); body.Append(new Paragraph( new Run(new Text("More Section 1 content...")))); // --- Section 1 break: sectPr inside a paragraph's pPr --- // This paragraph is the LAST paragraph of Section 1. var sect1Pr = new SectionProperties( new HeaderReference { Type = HeaderFooterValues.Default, Id = mainPart.GetIdOfPart(header1Part) }, // Section break type: start next section on a new page new SectionType { Val = SectionMarkValues.NextPage }); // Page size and margins for section 1 (required for valid sectPr) sect1Pr.Append(new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = (UInt32Value)12240U, // Letter width: 8.5" = 12240 DXA Height = (UInt32Value)15840U // Letter height: 11" = 15840 DXA }); sect1Pr.Append(new PageMargin { Top = 1440, Bottom = 1440, Left = (UInt32Value)1440U, Right = (UInt32Value)1440U }); // Wrap the sectPr in a paragraph's ParagraphProperties var sectionBreakPara = new Paragraph( new ParagraphProperties(sect1Pr)); body.Append(sectionBreakPara); // --- Section 2 content --- body.Append(new Paragraph( new Run(new Text("This is content in Section 2.")))); body.Append(new Paragraph( new Run(new Text("More Section 2 content...")))); // --- Final section: sectPr as last child of Body --- // This is the sectPr for the LAST section of the document. var finalSectPr = new SectionProperties( new HeaderReference { Type = HeaderFooterValues.Default, Id = mainPart.GetIdOfPart(header2Part) }); finalSectPr.Append(new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = (UInt32Value)12240U, Height = (UInt32Value)15840U }); finalSectPr.Append(new PageMargin { Top = 1440, Bottom = 1440, Left = (UInt32Value)1440U, Right = (UInt32Value)1440U }); body.Append(finalSectPr); } }