# OpenXML SDK C# Code Encyclopedia
Complete, heavily commented C# code patterns for DocumentFormat.OpenXml 3.x / .NET 8+ / C# 12.
**Namespace aliases used throughout:**
```csharp
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using A = DocumentFormat.OpenXml.Drawing;
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
using M = DocumentFormat.OpenXml.Math;
using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
```
**EMU conversion reference** (used throughout image/shape code):
```
1 inch = 914400 EMU
1 cm = 360000 EMU
1 pixel @ 96dpi = 9525 EMU
1 pt = 12700 EMU
```
---
## Table of Contents
1. [Table of Contents (TOC)](#1-table-of-contents-toc)
2. [Footnotes and Endnotes](#2-footnotes-and-endnotes)
3. [Field Codes — Comprehensive](#3-field-codes--comprehensive)
4. [Track Changes / Revisions](#4-track-changes--revisions)
5. [Comments (4-File System)](#5-comments-4-file-system)
6. [Images — Deep Dive](#6-images--deep-dive)
7. [Drawing Shapes (Non-Image)](#7-drawing-shapes-non-image)
8. [Math / Equations (OMML)](#8-math--equations-omml)
9. [Numbering System — Deep Dive](#9-numbering-system--deep-dive)
10. [Document Protection & Encryption](#10-document-protection--encryption)
---
## 1. Table of Contents (TOC)
### 1.1 Basic TOC Field (SimpleField Pattern)
The simplest way to insert a TOC. Uses `SimpleField` which wraps the entire field in one element.
```csharp
// Creates:
//
//
//
//
//
// TOC \o "1-3" \h \z \u
//
//
//
//
//
// Update this field to generate table of contents.
//
//
//
//
//
var tocParagraph = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
// Placeholder text shown before update
new Run(new Text("Update this field to generate table of contents.") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
body.Append(tocParagraph);
```
**TOC switch reference:**
| Switch | Meaning |
|--------|---------|
| `\o "1-3"` | Include outline levels 1–3 (customize as needed) |
| `\h` | Make entries hyperlinks (clickable) |
| `\z` | Hide tab leader and page numbers in Web Layout view |
| `\u` | Use applied paragraph outline level |
| `\f` | TOC entry from bookmark |
| `\t "style1,style2"` | Use custom styles instead of outline levels |
| `\n "1-2"` | Omit page numbers for levels 1–2 |
### 1.2 TOC Field with SdtBlock Wrapper
Wrapping a TOC in a Structured Document Tag (SdtBlock) enables rich content control features.
```csharp
// SdtBlock wrapper provides:
// - Ability to repeat/remove the entire TOC
// - Richer programmatic control
// - "Content Control" appearance in Word UI
var sdtBlock = new SdtBlock(
// SdtProperties defines the control's identity and behavior
new SdtProperties(
new SdtAlias { Val = "Table of Contents" },
new Tag { Val = "toc" },
new SdtContentText() // Plain text content
),
// SdtContentBlock contains the actual TOC field
new SdtContentBlock(
new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TOC \\o \"1-2\" \\h \\z ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Press F9 or right-click and select 'Update Field'") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
)
)
);
body.Append(sdtBlock);
```
### 1.3 TOC with Custom Heading Levels
Use the `\t` switch to build a TOC from arbitrary styles (not just Heading 1–9).
```csharp
// TOC using custom style names instead of outline levels:
// \t switch format: "style1,level1,style2,level2,..."
// This uses CustomHeading1 and CustomHeading2 styles mapped to TOC levels
var customTocPara = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TOC \\t \"CustomHeading1,1,CustomHeading2,2,CustomHeading3,3\" \\h \\z ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Update to see entries from CustomHeading1/2/3 styles.") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
### 1.4 TOC with Hyperlinks (\h switch)
The `\h` switch makes TOC entries clickable hyperlinks. This requires the entries to have a hyperlink anchor.
```csharp
// When \h is used, Word generates internal hyperlinks to each heading.
// The target is the bookmark automatically created by Word for headings.
// This is the standard pattern — no additional work needed in the field code itself.
var tocWithHyperlinks = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
// In Web Layout/Print Layout, Word will populate this with real entries
// Each entry will be a hyperlink pointing to the heading's internal bookmark
new Run(new Text("Click to update...") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
### 1.5 Auto-Update TOC on Document Open
You cannot programmatically update a TOC field's content (Word does this on open). Instead, tell Word to update fields automatically.
```csharp
// Method 1: Via DocumentSettingsPart — UpdateFieldsOnOpen
var settingsPart = mainDocumentPart.AddNewPart();
settingsPart.Settings = new Settings(
new UpdateFieldsOnOpen { Val = true } // Triggers field update on open
);
settingsPart.Settings.Save();
// Method 2: Field code includes \w to preserve formatting changes
// (Field code approach is limited — you still need Word to evaluate it)
// GOTCHA: OpenXML SDK cannot evaluate field codes.
// Word evaluates fields on open. Other readers (e.g., LibreOffice) may not.
// The document opens without content until the user explicitly updates.
```
### 1.6 TOC Styles — Custom TOC1, TOC2, TOC3 Styles with Leaders
Define custom TOC styles with indentation, tab leaders, and proper formatting.
```csharp
// First, add TOC1/TOC2/TOC3 styles to StyleDefinitionsPart
var stylesPart = mainDocumentPart.AddNewPart();
var styles = new Styles();
// TOC1 — Top-level entry (e.g., "1. Heading")
var toc1Style = new Style(
new StyleName { Val = "toc 1" },
new BasedOn { Val = "Normal" },
new PrimaryStyle(),
new StyleParagraphProperties(
new SpacingBetweenLines { Before = "120", After = "60" },
new Tabs(new TabStop { Val = TabStopValues.Right, Leader = TabStopLeaderCharValues.Dot, Position = 9072 }) // 5 inches right-aligned with dot leader
),
new StyleRunProperties(
new Bold(),
new FontSize { Val = "24" }, // 12pt
new FontSizeComplexScript { Val = "24" }
)
) { Type = StyleValues.Paragraph, StyleId = "TOC1" };
styles.Append(toc1Style);
// TOC2 — Second-level entry (e.g., "1.1 Subheading")
var toc2Style = new Style(
new StyleName { Val = "toc 2" },
new BasedOn { Val = "Normal" },
new PrimaryStyle(),
new StyleParagraphProperties(
new Indentation { Left = "220", Hanging = "220" }, // 0.15" indent, hang to align after number
new SpacingBetweenLines { Before = "60", After = "40" },
new Tabs(new TabStop { Val = TabStopValues.Right, Leader = TabStopLeaderCharValues.Dot, Position = 9072 })
),
new StyleRunProperties(
new FontSize { Val = "20" }, // 10pt
new FontSizeComplexScript { Val = "20" }
)
) { Type = StyleValues.Paragraph, StyleId = "TOC2" };
styles.Append(toc2Style);
// TOC3 — Third-level entry
var toc3Style = new Style(
new StyleName { Val = "toc 3" },
new BasedOn { Val = "Normal" },
new PrimaryStyle(),
new StyleParagraphProperties(
new Indentation { Left = "440", Hanging = "440" }, // 0.3" indent
new SpacingBetweenLines { Before = "40", After = "20" },
new Tabs(new TabStop { Val = TabStopValues.Right, Leader = TabStopLeaderCharValues.Dot, Position = 9072 })
),
new StyleRunProperties(
new Italic(),
new FontSize { Val = "20" },
new FontSizeComplexScript { Val = "20" }
)
) { Type = StyleValues.Paragraph, StyleId = "TOC3" };
styles.Append(toc3Style);
stylesPart.Styles = styles;
stylesPart.Styles.Save();
// Now use \t switch to reference these styles in the TOC field:
// TOC \t "TOC1,1,TOC2,2,TOC3,3" \h \z
```
**Tab leader options:** `TabStopLeaderCharValues.Dot` (........), `TabStopLeaderCharValues.Dash` (--------), `TabStopLeaderCharValues.Underscore` (________), `TabStopLeaderCharValues.MiddleDot` (·······).
### 1.7 Mini TOC for a Section
A mini TOC covers only a portion of the document using a bookmark-scoped `\f` switch.
```csharp
// Step 1: Define a bookmark around the section to be covered
var sectionStart = new BookmarkStart { Id = "10", Name = "_Section1TOC" };
var sectionEnd = new BookmarkEnd { Id = "10" };
// Step 2: Put heading paragraphs inside the bookmark range
var headingPara = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }),
new Run(new Text("Section A: Introduction"))
);
// Full section wrapped in bookmark
body.Append(sectionStart);
body.Append(headingPara);
body.Append(sectionEnd);
// Step 3: Mini TOC field references that bookmark with \f switch
var miniTocPara = new Paragraph(
new Run(new Text("In this section: ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TOC \\f _Section1TOC ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Mini TOC placeholder") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
---
## 2. Footnotes and Endnotes
### 2.1 FootnotesPart — Initialization
Footnotes in Word require a dedicated `FootnotesPart`. The first three footnotes are special: separator, continuation separator, and continuation notice.
```csharp
// Initialize the FootnotesPart
var footnotesPart = mainDocumentPart.AddNewPart();
var footnotes = new Footnotes();
// CRITICAL: Footnotes must start with these 3 special footnotes:
// 1. Separator (id="0") — thin line between main text and footnotes
// 2. ContinuationSeparator (id="1") — thick line when footnotes continue to next column/page
// 3. ContinuationNotice (id="2") — "..." text when footnotes overflow
// Footnote ID=0: Separator (appears between main text and first footnote)
var separatorFootnote = new Footnote(
new Paragraph(
new ParagraphProperties(
new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }
),
// The separator is just a paragraph with a border at the bottom
new Paragraph(
new Run(new Separator())
)
)
) { Type = FootnoteEndnoteValues.Separator, Id = 0 };
footnotes.Append(separatorFootnote);
// Footnote ID=1: Continuation Separator
var continuationSepFootnote = new Footnote(
new Paragraph(
new ParagraphProperties(
new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }
),
new Run(new ContinuationSeparator())
)
) { Type = FootnoteEndnoteValues.ContinuationSeparator, Id = 1 };
footnotes.Append(continuationSepFootnote);
// Footnote ID=2: Continuation Notice (optional, appears when footnotes overflow)
var continuationNoticeFootnote = new Footnote(
new Paragraph(
new ParagraphProperties(
new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }
),
new Run(new Text("...") { Space = SpaceProcessingModeValues.Preserve })
)
) { Type = FootnoteEndnoteValues.ContinuationNotice, Id = 2 };
footnotes.Append(continuationNoticeFootnote);
footnotesPart.Footnotes = footnotes;
footnotesPart.Footnotes.Save();
```
### 2.2 Adding a Normal Footnote
Place a `FootnoteReference` in the document body and corresponding `Footnote` content in `FootnotesPart`.
```csharp
// In the document body, at the insertion point:
var footnoteRefRun = new Run(
new RunProperties(
new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }
),
new FootnoteReference { Id = 3 } // ID must match the Footnote's Id
);
body.Append(new Paragraph(
new Run(new Text("Some text with a footnote marker.") { Space = SpaceProcessingModeValues.Preserve }),
footnoteRefRun
));
// In FootnotesPart, add the corresponding footnote content:
//
//
//
//
// This is the footnote text.
//
//
var newFootnote = new Footnote { Id = 3 };
newFootnote.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "FootnoteText" }),
new Run(new FootnoteReferenceMark()), // Small superscript mark
new Run(new Text(" This is the footnote text.") { Space = SpaceProcessingModeValues.Preserve })
));
footnotesPart.Footnotes!.Append(newFootnote);
footnotesPart.Footnotes.Save();
```
### 2.3 Footnote with Custom Mark (Asterisk, Symbol)
Override the default auto-numbering with a custom symbol.
```csharp
// Use FootnoteReferenceMark (the automatic symbol) OR provide a custom character.
// For custom marks, use a regular Run with the symbol character instead of FootnoteReferenceMark.
// Footnote ID=4 with custom asterisk mark
var customFootnote = new Footnote { Id = 4 };
customFootnote.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "FootnoteText" }),
// Custom mark: use a bold asterisk from Symbol font
new Run(
new RunProperties(
new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }
),
new RunFonts { Ascii = "Symbol", HighAnsi = "Symbol" },
new Text("*")
),
new Run(new Text(" Custom footnote with symbol mark.") { Space = SpaceProcessingModeValues.Preserve })
));
footnotesPart.Footnotes!.Append(customFootnote);
// In document body, at the insertion point:
var customFootnoteRef = new Run(
new RunProperties(
new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }
),
new FootnoteReference { Id = 4 }
);
```
### 2.4 FootnotePosition — Placement via SectionProperties
Control whether footnotes appear at the bottom of each page or beneath the text.
```csharp
// Add FootnoteProperties to SectionProperties
var sectPr = body.Elements().First()
?? body.AppendChild(new SectionProperties());
// Footnote placement:
// - BottomOfPage (default) — footnotes appear at bottom of page
// - BeneathText — footnotes appear immediately below the last text on the page
sectPr.Append(new FootnoteProperties(
new FootnotePosition { Val = FootnotePositionValues.BeneathText }
));
// Footnote numbering restart options:
// - RestartAtSection — restart numbering at each section
// - RestartAtPage — restart at each page (Word default for footnotes)
// - Continuous — don't restart (number sequentially through document)
sectPr.Append(new FootnoteProperties(
new FootnotePosition { Val = FootnotePositionValues.BottomOfPage },
new FootnoteNumberingFormat { Val = NumberFormatValues.Decimal }, // 1, 2, 3...
new FootnoteNumberingStart { Val = 1 }, // Start at 1
new FootnoteNumberingRestart { Val = FootnoteRestartValues.RestartAtPage }
));
```
### 2.5 EndnotesPart — Same Pattern
Endnotes follow the exact same structure as footnotes but use `EndnotesPart`.
```csharp
var endnotesPart = mainDocumentPart.AddNewPart();
var endnotes = new Endnotes();
// Endnote ID=0: Separator (same pattern as footnotes)
var endnoteSeparator = new Endnote(
new Paragraph(new Run(new Separator()))
) { Type = FootnoteEndnoteValues.Separator, Id = 0 };
endnotes.Append(endnoteSeparator);
// Endnote ID=1: ContinuationSeparator
var endnoteContSep = new Endnote(
new Paragraph(new Run(new ContinuationSeparator()))
) { Type = FootnoteEndnoteValues.ContinuationSeparator, Id = 1 };
endnotes.Append(endnoteContSep);
endnotesPart.Endnotes = endnotes;
endnotesPart.Endnotes.Save();
// In document body, use EndnoteReference instead of FootnoteReference
var endnoteRefRun = new Run(
new RunProperties(
new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }
),
new EndnoteReference { Id = 3 }
);
body.Append(new Paragraph(
new Run(new Text("An endnote marker.") { Space = SpaceProcessingModeValues.Preserve }),
endnoteRefRun
));
// Corresponding Endnote content in EndnotesPart
var newEndnote = new Endnote { Id = 3 };
newEndnote.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "EndnoteText" }),
new Run(new EndnoteReferenceMark()),
new Run(new Text(" This is the endnote content.") { Space = SpaceProcessingModeValues.Preserve })
));
endnotesPart.Endnotes!.Append(newEndnote);
```
### 2.6 Endnote Placement via SectionProperties
```csharp
// EndnoteProperties on SectionProperties controls endnote placement
sectPr.Append(new EndnoteProperties(
new EndnotePosition { Val = EndnotePositionValues.EndOfDocument } // Default
// Other options: EndOfSection, BeneathText (rarely used for endnotes)
));
```
---
## 3. Field Codes — Comprehensive
### 3.1 SimpleField vs Complex Field Architecture
**SimpleField** — single element, easier to write but less control:
```csharp
// 1
new SimpleField(new Run(new Text("1"))) { Instruction = " PAGE " }
```
**Complex Field (Begin/Separate/End)** — full control over each field component:
```csharp
//
// PAGE
//
// 1
//
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("1")), // Cached result shown until update
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
```
**Key differences:**
- `SimpleField` is one `w:fldSimple` element containing one `w:r`
- Complex field uses `FieldChar` with `FieldCharValues.Begin/Separate/End` to delimit regions
- `FieldCode` is `w:instrText` — contains the field instruction string
- The text between `Separate` and `End` is the "cached result" shown before update
- After `Separate`, `FieldCode` contains the switches that define field behavior
### 3.2 PAGE, NUMPAGES, DATE, TIME
**PAGE — current page number:**
```csharp
// SimpleField version
new SimpleField(new Run(new Text("1"))) { Instruction = " PAGE " }
// Complex field version
new Paragraph(
new Run(new Text("Page ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("1")), // Cached value
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
**NUMPAGES — total page count:**
```csharp
new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" NUMPAGES ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("10")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text(" pages") { Space = SpaceProcessingModeValues.Preserve })
);
```
**DATE — current date with format switch:**
```csharp
// DATE with custom format: \@ "yyyy-MM-dd"
// The \@ switch specifies the date picture
new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" DATE \\@ \"yyyy-MM-dd\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("2026-03-22")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// DATE with time: \@ "MMMM d, yyyy h:mm AM/PM"
new Run(new FieldCode(" DATE \\@ \"MMMM d, yyyy h:mm AM/PM\" ") { Space = SpaceProcessingModeValues.Preserve }),
// DATE with locale: \* MERGEFORMAT preserves formatting on update
new Run(new FieldCode(" DATE \\@ \"d/M/yyyy\" \\* MERGEFORMAT ") { Space = SpaceProcessingModeValues.Preserve }),
```
**TIME — current time:**
```csharp
new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TIME \\@ \"HH:mm:ss\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("14:30:00")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
### 3.3 FILENAME, AUTHOR, TITLE (Document Properties)
These fields pull from the document's core properties.
```csharp
// FILENAME — document filename
new Run(new FieldCode(" FILENAME ") { Space = SpaceProcessingModeValues.Preserve }),
// FILENAME with path: \* MERGEFORMAT
new Run(new FieldCode(" FILENAME \\* MERGEFORMAT ") { Space = SpaceProcessingModeValues.Preserve }),
// AUTHOR — from document core properties
new Run(new FieldCode(" AUTHOR ") { Space = SpaceProcessingModeValues.Preserve }),
// TITLE — from document core properties
new Run(new FieldCode(" TITLE ") { Space = SpaceProcessingModeValues.Preserve }),
// SUBJECT — from document core properties
new Run(new FieldCode(" SUBJECT ") { Space = SpaceProcessingModeValues.Preserve }),
// Keywords — from document core properties
new Run(new FieldCode(" KEYWORDS ") { Space = SpaceProcessingModeValues.Preserve }),
// All document property fields
new Paragraph(
new Run(new Text("Title: ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" TITLE ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("My Document")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
**Set document properties programmatically:**
```csharp
// Set core properties via PackageProperties (OfficePackage)
var package = doc.ExtendedFilePropertiesPart?.Properties;
if (package != null)
{
package.Creator = "Author Name";
package.Title = "Document Title";
package.Subject = "Subject";
package.Description = "Description";
package.Keywords = "keyword1, keyword2";
package.Save();
}
```
### 3.4 REF — Cross-Reference to Bookmark
`REF` retrieves the text of a bookmarked paragraph or the value of a REF field.
```csharp
// First, create a bookmark around some content
var bookmarkStart = new BookmarkStart { Id = "100", Name = "Figure1Caption" };
var bookmarkEnd = new BookmarkEnd { Id = "100" };
var captionPara = new Paragraph(
new Run(new Text("Figure 1: Architecture diagram") { Space = SpaceProcessingModeValues.Preserve })
);
body.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Caption" }),
bookmarkStart,
new Run(new Text("Figure 1: Architecture diagram")),
bookmarkEnd
));
// Now reference it with REF field
var refField = new Paragraph(
new Run(new Text("As shown in ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" REF Figure1Caption \\* MERGEFORMAT ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Figure 1: Architecture diagram")), // Cached result
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text(", the system consists of...") { Space = SpaceProcessingModeValues.Preserve })
);
```
**REF switches:**
| Switch | Effect |
|--------|--------|
| `\r` | Insert bookmarked text but as hyperlink |
| `\h` | Make REF a hyperlink to the bookmark |
| `\n` | Suppress paragraph number |
| `\p` | Show relative position (above/below) |
| `\t` | Suppress trailing spaces |
| `\* MERGEFORMAT` | Preserve formatting |
### 3.5 SEQ — Sequence Numbering for Figures/Tables
`SEQ` generates auto-incrementing numbers for elements like figures, tables, and listings.
```csharp
// First figure caption
var fig1Caption = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Caption" }),
new Run(new Text("Figure ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" SEQ Figure \\* ARABIC ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("1")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text(": System Architecture") { Space = SpaceProcessingModeValues.Preserve })
);
// Second figure (Word auto-increments)
var fig2Caption = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Caption" }),
new Run(new Text("Figure ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" SEQ Figure \\* ARABIC ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("2")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text(": Data Flow") { Space = SpaceProcessingModeValues.Preserve })
);
// Reference a figure number
var figRef = new Paragraph(
new Run(new Text("See Figure ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" SEQ Figure \\* ARABIC ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("1")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text(" above.") { Space = SpaceProcessingModeValues.Preserve })
);
// SEQ sequence identifier: "Figure" can be any name
// Multiple sequences: SEQ Figure, SEQ Table, SEQ Listing are independent
```
### 3.6 HYPERLINK — Internal and External Links
```csharp
// External hyperlink (to URL)
var extHyperlinkRel = mainDocumentPart.AddHyperlinkRelationship(
new Uri("https://example.com"), true);
var extHyperlink = new Hyperlink(
new Run(
new RunProperties(new Color { Val = "0563C1" }, new Underline { Val = UnderlineValues.Single }),
new Text("Visit Example.com")
)
) { Id = extHyperlinkRel.Id }; // Id references the relationship
// Internal hyperlink (to bookmark)
var intHyperlink = new Hyperlink(
new Run(
new RunProperties(new Color { Val = "0563C1" }, new Underline { Val = UnderlineValues.Single }),
new Text("Go to Chapter 1")
)
) { Anchor = "Chapter1Bookmark" }; // Anchor = bookmark name
// HYPERLINK field for advanced cases (with screen tip)
var hyperlinkedField = new Run(
new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" HYPERLINK \\l \"Chapter1Bookmark\" \\t \"_top\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Go to Chapter 1")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// \l = target (anchor for internal, URL for external)
// \t = target frame (optional, e.g., "_top" to open in same window)
```
### 3.7 MERGEFIELD — Mail Merge
```csharp
// MERGEFIELD uses a special syntax: MERGEFIELD FieldName
// The field name must match a mail merge data source column name
// Simple MERGEFIELD
var mergeFieldPara = new Paragraph(
new Run(new Text("Dear ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" MERGEFIELD FirstName ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("«FirstName»")), // «» are Word's placeholder markers
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text(",") { Space = SpaceProcessingModeValues.Preserve })
);
// Full name with formatting
var mergeFieldWithFormat = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" MERGEFIELD FullName \\* UPPERCASE ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("«FullName»")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// To actually perform mail merge, use Word's MailMerge settings
var settingsPart = mainDocumentPart.AddNewPart();
settingsPart.Settings = new Settings(
new MailMerge(
new DataType { Val = MailMergeDataValues.TextFile },
new DataSourceReference { Id = "rIdForDataSource" }
)
);
```
### 3.8 IF Field — Conditional Text
The `IF` field evaluates a condition and displays one of two text values. Commonly used with `MERGEFIELD`.
```csharp
// IF Field syntax: IF [expression] [operator] [value] "true_text" "false_text"
// Often combined with MERGEFIELD:
// If the recipient's region equals "USA", show "Dear Customer", otherwise "Dear Valued Customer"
var ifFieldPara = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
// Complex nested: { IF { MERGEFIELD Region } = "USA" "Dear American Customer" "Dear Customer" }
new Run(new FieldCode(" IF ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }), // Nested MERGEFIELD begin
new Run(new FieldCode(" MERGEFIELD Region ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("«Region»")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }), // Nested MERGEFIELD end
new Run(new FieldCode(" = \"USA\" \"Dear American Customer\" \"Dear Customer\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Dear Customer")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// Note: Nested fields within IF are tricky with complex field syntax.
// A simpler approach: use two separate IF fields checking a bookmark value.
```
### 3.9 STYLEREF — Reference Heading Text
`STYLEREF` displays the text of the nearest paragraph with a specified style — useful for running headers.
```csharp
// STYLEREF Heading1 — inserts the text of the most recent Heading1 paragraph
// Great for running headers that show the current chapter
// Running header in footer
var footerPart = mainDocumentPart.AddNewPart();
footerPart.Footer = new Footer(
new Paragraph(
new ParagraphProperties(
new Justification { Val = JustificationValues.Right }
),
// Left-aligned: chapter heading
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" STYLEREF \"Heading 1\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Chapter Title")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
new Run(new Text("\t") { Space = SpaceProcessingModeValues.Preserve }), // Tab
// Right-aligned: page number
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("1")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
)
);
// STYLEREF with \n switch to suppress paragraph numbering
new Run(new FieldCode(" STYLEREF \"Heading 1\" \\n ") { Space = SpaceProcessingModeValues.Preserve }),
// STYLEREF with \p switch to show relative position
new Run(new FieldCode(" STYLEREF \"Heading 2\" \\p ") { Space = SpaceProcessingModeValues.Preserve }),
```
### 3.10 SET and ASK Fields
`SET` stores a value in a variable. `ASK` prompts the user and stores their response.
```csharp
// SET — define a document variable (accessed via DOCPROPERTY or REF)
var setField = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" SET MyVariable \"some value\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// REF to read the variable
var refMyVar = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" REF MyVariable ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("some value")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// ASK — prompt user for input when field is updated
// Note: ASK displays a dialog box when updated
var askField = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" ASK AuthorName \"Enter author name:\" ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new FieldChar { FieldCharType = FieldCharValues.End }),
// REF to display the stored value
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" REF AuthorName ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("Author Name")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
```
### 3.11 Calculated Fields (= Expressions)
The `=` field evaluates arithmetic expressions.
```csharp
// = field with arithmetic
var calcPara = new Paragraph(
new Run(new Text("Total: $") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" = 100 + 250 - 30 ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("320")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// = with multiplication using SEQ references
var calcWithSeq = new Paragraph(
new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }),
new Run(new FieldCode(" = 3 * 5 ") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }),
new Run(new Text("15")),
new Run(new FieldChar { FieldCharType = FieldCharValues.End })
);
// Combine with formatting
new Run(new FieldCode(" = 1000 * 1.08 \\# \"#,##0.00\" ") { Space = SpaceProcessingModeValues.Preserve }),
// \# switch applies number format to result
```
### 3.12 UpdateFieldsOnOpen — Automatic Field Updates
```csharp
// Settings that trigger field updates when document opens
var settingsPart = mainDocumentPart.AddNewPart();
settingsPart.Settings = new Settings(
// Update all fields (TOC, REF, PAGE, etc.) on open
new UpdateFieldsOnOpen { Val = true }
);
settingsPart.Settings.Save();
// Additional field-related settings:
var additionalSettings = new Settings(
// Auto-format fractions: 1/2 → ½
new AutomaticAdjustmentOfFontSizesToFitDocument(),
// True: use field codes instead of cached values on update
new UseXSLTWhenSaving(),
// Mail merge settings
new MailMerge(
new MainDocumentType { Val = MailMergeDocumentValues.FormLetters }
)
);
```
---
## 4. Track Changes / Revisions
### 4.1 Enabling Track Changes
Track changes must be explicitly enabled via DocumentSettingsPart.
```csharp
var settingsPart = mainDocumentPart.AddNewPart();
settingsPart.Settings = new Settings(
// Enable track changes — any edit will be tracked
new TrackRevisions()
);
// Also recommended: prevent fields from being updated during tracking
settingsPart.Settings.Append(new DonNotTrackFormatting());
```
### 4.2 InsertedRun (w:ins) — Tracked Insertion
```csharp
//
//
// Inserted text.
//
//
var insertedText = new InsertedRun(
new Run(
new Text("Inserted text.") { Space = SpaceProcessingModeValues.Preserve }
)
)
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 10, 0, 0, DateTimeKind.Utc),
Id = "5"
};
var para = new Paragraph(
new Run(new Text("Existing text. ") { Space = SpaceProcessingModeValues.Preserve }),
insertedText,
new Run(new Text(" More existing text.") { Space = SpaceProcessingModeValues.Preserve })
);
```
### 4.3 DeletedRun (w:del) — Tracked Deletion
**CRITICAL: Inside `w:del`, text MUST be `DeletedText` (`w:delText`), NOT `Text` (`w:t`)!**
```csharp
//
//
//
// Deleted text.
//
//
var deletedRun = new DeletedRun(
new Run(
new RunProperties(new Bold()),
new DeletedText("Deleted text.") { Space = SpaceProcessingModeValues.Preserve }
)
)
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 10, 5, 0, DateTimeKind.Utc),
Id = "6"
};
var para = new Paragraph(
new Run(new Text("Keep this. ") { Space = SpaceProcessingModeValues.Preserve }),
deletedRun,
new Run(new Text(" Keep this too.") { Space = SpaceProcessingModeValues.Preserve })
);
// GOTCHA: Never use inside — use only.
// Using w:t inside w:del causes corruption or silent repair by Word.
```
### 4.4 RunPropertiesChange — Formatting Change Tracking
Records that a run's formatting was changed. The `w:rPrChange` goes inside `w:rPr`.
```csharp
//
//
//
//
//
//
//
// Formatted text.
//
// The current (new) formatting is in the outer w:rPr
// The old (previous) formatting is in the w:rPrChange child
var formattedTextRun = new Run(
new RunProperties(
new Bold(), // New formatting: now bold
new RunPropertiesChange( // Records the old formatting (empty = not bold)
new RunProperties() // Empty = previously had no formatting
)
{
Author = "Bob",
Date = new DateTime(2026, 3, 22, 11, 0, 0, DateTimeKind.Utc),
Id = "7"
}
),
new Text("Formatted text.") { Space = SpaceProcessingModeValues.Preserve }
);
```
### 4.5 ParagraphPropertiesChange
Records that paragraph-level properties were changed.
```csharp
//
//
//
//
//
//
//
//
var changedPara = new Paragraph(
new ParagraphProperties(
new Justification { Val = JustificationValues.Center }, // New
new ParagraphPropertiesChange(
new ParagraphProperties(
new Justification { Val = JustificationValues.Left } // Old
)
)
{
Author = "Bob",
Date = new DateTime(2026, 3, 22, 11, 5, 0, DateTimeKind.Utc),
Id = "8"
}
),
new Run(new Text("Centered paragraph."))
);
```
### 4.6 ParagraphMarkRunPropertiesChange
Records that the paragraph mark's formatting (trailing formatting) was changed.
```csharp
//
//
//
//
//
//
//
//
//
//
//
//
//
```
### 4.7 Table Revision Marks
```csharp
// TableRowInsertionRevision — a row was inserted
//
//
//
var insertedRow = new TableRow(
new TableRowProperties(
new TableRowInsertionRevision
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 12, 0, 0, DateTimeKind.Utc),
Id = "10"
}
),
new TableCell(new Paragraph(new Run(new Text("New row cell"))))
);
// TableCellInsertionRevision — a cell was inserted
var insertedCell = new TableCell(
new TableCellProperties(
new TableCellInsertionRevision
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 12, 1, 0, DateTimeKind.Utc),
Id = "11"
}
),
new Paragraph(new Run(new Text("New cell")))
);
```
### 4.8 SectionPropertiesChange
```csharp
//
//
//
//
//
//
//
//
var changedSection = new SectionProperties(
new PageSize { Width = 16838U, Height = 11906U }, // New: A4
new SectionPropertiesChange(
new SectionProperties(
new PageSize { Width = 12240U, Height = 15840U } // Old: Letter
)
)
{
Author = "Bob",
Date = new DateTime(2026, 3, 22, 12, 30, 0, DateTimeKind.Utc),
Id = "12"
}
);
```
### 4.9 NumberingChange
```csharp
//
//
//
//
//
//
//
//
//
//
var changedNumbering = new NumberingProperties(
new NumberingLevelReference { Val = 0 },
new NumberingId { Val = 3 }, // New: numId 3
new NumberingChange(
new NumberingProperties(
new NumberingLevelReference { Val = 0 },
new NumberingId { Val = 1 } // Old: numId 1
)
)
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 13, 0, 0, DateTimeKind.Utc),
Id = "13"
}
);
```
### 4.10 Accepting All Revisions Programmatically
```csharp
// Accept all revisions: unwrap w:ins (keep content), remove w:del entirely
public static void AcceptAllRevisions(WordprocessingDocument doc)
{
var body = doc.MainDocumentPart?.Document?.Body;
if (body == null) return;
// Accept insertions: remove w:ins wrapper, keep inner runs
var insertions = body.Descendants().ToList();
foreach (var ins in insertions)
{
var parent = ins.Parent;
if (parent == null) continue;
var children = ins.ChildElements.ToList();
foreach (var child in children)
{
child.Remove();
parent.InsertBefore(child, ins);
}
ins.Remove();
}
// Accept deletions: remove entire w:del element
var deletions = body.Descendants().ToList();
foreach (var del in deletions)
del.Remove();
}
// Also accept formatting changes:
// For w:rPrChange: replace the entire RunProperties with the "old" properties inside the change
// For w:pPrChange: replace with the old properties
```
### 4.11 Rejecting All Revisions Programmatically
```csharp
// Reject all revisions: unwrap w:del (restore text), remove w:ins entirely
public static void RejectAllRevisions(WordprocessingDocument doc)
{
var body = doc.MainDocumentPart?.Document?.Body;
if (body == null) return;
// Reject insertions: remove entire w:ins element and its content
var insertions = body.Descendants().ToList();
foreach (var ins in insertions)
ins.Remove();
// Reject deletions: unwrap w:del, convert w:delText back to w:t
var deletions = body.Descendants().ToList();
foreach (var del in deletions)
{
var parent = del.Parent;
if (parent == null) continue;
foreach (var run in del.Elements().ToList())
{
foreach (var delText in run.Elements().ToList())
{
var text = new Text(delText.Text) { Space = delText.Space };
delText.InsertAfterSelf(text);
delText.Remove();
}
run.Remove();
parent.InsertBefore(run, del);
}
del.Remove();
}
}
```
### 4.12 MoveFrom / MoveTo — Tracked Text Moving
```csharp
// MoveFrom (w:moveFrom) marks the origin of moved text
// MoveTo (w:moveTo) marks the destination
// Both must have the same w:id
//
// Text that was moved.
//
// At destination:
//
// Text that was moved.
//
var movedFrom = new MoveFromRun(
new Run(new Text("Text that was moved.") { Space = SpaceProcessingModeValues.Preserve })
)
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 14, 0, 0, DateTimeKind.Utc),
Id = "14"
};
var movedTo = new MoveToRun(
new Run(new Text("Text that was moved.") { Space = SpaceProcessingModeValues.Preserve })
)
{
Author = "Alice",
Date = new DateTime(2026, 3, 22, 14, 0, 0, DateTimeKind.Utc),
Id = "14"
};
```
### 4.13 RevisionId Generation
All revision elements need unique, monotonically increasing integer IDs.
```csharp
public static int GetNextRevisionId(Body body)
{
int maxId = 0;
foreach (var elem in body.Descendants())
{
// Check common revision element types for Id attribute
var idAttr = elem.GetAttributes()
.FirstOrDefault(a => a.LocalName == "id" &&
(elem is InsertedRun or DeletedRun or DeletedText or
MoveFromRun or MoveToRun or RunPropertiesChange or
ParagraphPropertiesChange or SectionPropertiesChange or
TableRowInsertionRevision or TableCellInsertionRevision));
if (idAttr.Value != null && int.TryParse(idAttr.Value, out int id) && id > maxId)
maxId = id;
}
return maxId + 1;
}
// Simpler approach: scan all elements with "id" attribute in the document
public static int GetNextRevisionIdSimple(Body body)
{
int maxId = 0;
foreach (var elem in body.Descendants())
{
foreach (var attr in elem.GetAttributes())
{
if (attr.LocalName == "id" && int.TryParse(attr.Value, out int id) && id > maxId)
maxId = id;
}
}
return maxId + 1;
}
```
---
## 5. Comments (4-File System)
### 5.1 Full 4-File Comment System Setup
Comments require four XML files plus markers in `document.xml`.
```csharp
// This method creates a complete comment with all 4 files properly initialized
public static int AddFullComment(
WordprocessingDocument doc,
string text,
string author,
string initials,
string rangeText,
int? existingCommentId = null)
{
var mainPart = doc.MainDocumentPart
?? throw new InvalidOperationException("Document has no MainDocumentPart.");
int commentId = existingCommentId ?? GetNextCommentId(doc);
// Generate paraId (8-char hex) and durableId (8-digit hex)
string paraId = Guid.NewGuid().ToString("N")[..8].ToUpperInvariant();
string durableId = new Random().Next(0x10000000, 0xFFFFFFFF).ToString("X8");
var body = mainPart.Document!.Body!;
// ─────────────────────────────────────────────────────────────
// FILE 1: word/comments.xml — Main comment content
// ─────────────────────────────────────────────────────────────
var commentsPart = mainPart.WordprocessingCommentsPart
?? mainPart.AddNewPart();
if (commentsPart.Comments == null)
commentsPart.Comments = new Comments();
// Create a paragraph for the comment with a unique paraId (via w14:paraId)
var commentPara = new Paragraph(
new ParagraphProperties(
new ParagraphStyleId { Val = "CommentText" },
// w14:paraId for modern comment threading
new乳啜攠嘶嘐呓顾纨asiId { Val = paraId }
),
new Run(
new RunProperties(new RunStyle { Val = "CommentReference" }),
new AnnotationReferenceMark()
),
new Run(new Text(text))
);
var comment = new Comment
{
Id = commentId.ToString(),
Author = author,
Date = DateTime.UtcNow,
Initials = initials
};
comment.Append(commentPara);
commentsPart.Comments.Append(comment);
commentsPart.Comments.Save();
// ─────────────────────────────────────────────────────────────
// FILE 2: word/commentsExtended.xml — W15 extensions (paraId, done status)
// ─────────────────────────────────────────────────────────────
var commentsExPart = mainPart.WordprocessingCommentsExPart
?? mainPart.AddNewPart();
if (commentsExPart.CommentsEx == null)
commentsExPart.CommentsEx = new CommentExCollection();
// w15:commentEx links the comment to its paragraph and tracks done/resolved
var commentEx = new CommentEx
{
ParaId = new HexBinaryValue(paraId),
Done = new OnOffValue(false) // done="0" = not resolved
};
commentsExPart.CommentsEx.Append(commentEx);
commentsExPart.CommentsEx.Save();
// ─────────────────────────────────────────────────────────────
// FILE 3: word/commentsIds.xml — Persistent ID mapping
// ─────────────────────────────────────────────────────────────
var commentsIdsPart = mainPart.WordprocessingCommentsIdsPart
?? mainPart.AddNewPart();
if (commentsIdsPart.CommentsIds == null)
commentsIdsPart.CommentsIds = new CommentIds();
// w16cid:commentId maps paraId to a durable (globally unique) ID
var commentIdEntry = new CommentId
{
ParaId = new HexBinaryValue(paraId),
DurableId = durableId
};
commentsIdsPart.CommentsIds.Append(commentIdEntry);
commentsIdsPart.CommentsIds.Save();
// ─────────────────────────────────────────────────────────────
// FILE 4: word/commentsExtensible.xml — W16 extensible
// ─────────────────────────────────────────────────────────────
var commentsExtPart = mainPart.WordprocessingCommentsExtensiblePart
?? mainPart.AddNewPart();
if (commentsExtPart.CommentsExtensible == null)
commentsExtPart.CommentsExtensible = new CommentExtensibleCollection();
// w16cex:commentExtensible provides the durable ID with UTC timestamp
var extensibleEntry = new CommentExtensible
{
DurableId = durableId,
DateUtc = DateTime.UtcNow
};
commentsExtPart.CommentsExtensible.Append(extensibleEntry);
commentsExtPart.CommentsExtensible.Save();
// ─────────────────────────────────────────────────────────────
// document.xml — Insert range markers around the target text
// ─────────────────────────────────────────────────────────────
// commentRangeStart and commentRangeEnd bracket the commented text
// commentReference is a run containing the visible superscript number
var rangeStart = new CommentRangeStart { Id = commentId.ToString() };
var rangeEnd = new CommentRangeEnd { Id = commentId.ToString() };
var refRun = new Run(
new RunProperties(new RunStyle { Val = "CommentReference" }),
new CommentReference { Id = commentId.ToString() }
);
// Find the paragraph containing rangeText and insert markers
// Simple approach: append at end of body
body.Append(rangeStart);
body.Append(new Paragraph(new Run(new Text(rangeText))));
body.Append(rangeEnd);
body.Append(new Paragraph(refRun)); // The comment ref must be in its own paragraph
return commentId;
}
// Helper: get next comment ID
private static int GetNextCommentId(WordprocessingDocument doc)
{
var commentsPart = doc.MainDocumentPart?.WordprocessingCommentsPart;
if (commentsPart?.Comments == null) return 1;
int max = 0;
foreach (var c in commentsPart.Comments.Elements())
if (c.Id?.Value != null && int.TryParse(c.Id.Value, out int id) && id > max)
max = id;
return max + 1;
}
```
**The 4 files at a glance:**
| File | Part Class | Content | Key Attributes |
|------|-----------|---------|----------------|
| `comments.xml` | `WordprocessingCommentsPart` | Comment text | `w:id`, `w:author`, `w:date`, `w:initials` |
| `commentsExtended.xml` | `WordprocessingCommentsExPart` | W15 extensions | `w15:paraId`, `w15:done` |
| `commentsIds.xml` | `WordprocessingCommentsIdsPart` | Persistent IDs | `w16cid:paraId`, `w16cid:durableId` |
| `commentsExtensible.xml` | `WordprocessingCommentsExtensiblePart` | W16 extensible | `w16cex:durableId`, `w16cex:dateUtc` |
### 5.2 Comment Reply (Threaded Comments)
```csharp
// To add a reply, create a new comment and link it to the parent via commentsExtended.xml
public static int AddCommentReply(
WordprocessingDocument doc,
int parentCommentId,
string replyText,
string author,
string initials)
{
var mainPart = doc.MainDocumentPart!;
// Get parent's paraId from commentsExtended.xml
var commentsExPart = mainPart.WordprocessingCommentsExPart;
var parentParaId = "";
if (commentsExPart?.CommentsEx != null)
{
var parentCommentEx = commentsExPart.CommentsEx
.Elements()
.FirstOrDefault(ce =>
ce.Parent is Comment c &&
c.Id?.Value == parentCommentId.ToString());
// Actually need to cross-reference through paraId...
// Simpler: look up via comments.xml paraId
}
// Generate new IDs for the reply
int replyId = GetNextCommentId(doc);
string replyParaId = Guid.NewGuid().ToString("N")[..8].ToUpperInvariant();
string durableId = new Random().Next(0x10000000, 0xFFFFFFFF).ToString("X8");
// Add to comments.xml (new comment with same structure)
var commentsPart = mainPart.WordprocessingCommentsPart!;
var replyComment = new Comment
{
Id = replyId.ToString(),
Author = author,
Date = DateTime.UtcNow,
Initials = initials
};
replyComment.Append(new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "CommentText" }),
new Run(new RunProperties(new RunStyle { Val = "CommentReference" }), new AnnotationReferenceMark()),
new Run(new Text(replyText))
));
commentsPart.Comments!.Append(replyComment);
commentsPart.Comments.Save();
// KEY: In commentsExtended.xml, use paraIdParent to link to parent
var commentsEx = mainPart.WordprocessingCommentsExPart!;
var replyEx = new CommentEx
{
ParaId = new HexBinaryValue(replyParaId),
ParaIdParent = new HexBinaryValue(parentParaId), // Link to parent
Done = new OnOffValue(false)
};
commentsEx.CommentsEx!.Append(replyEx);
commentsEx.CommentsEx.Save();
// Add to commentsIds.xml and commentsExtensible.xml (same pattern as parent)
// ... (same as AddFullComment for these two files)
// Note: Replies do NOT need range markers in document.xml
// They appear threaded under the parent in Word's UI
return replyId;
}
```
### 5.3 Resolving a Comment
```csharp
// To resolve (mark done), set w15:done="1" in commentsExtended.xml
public static void ResolveComment(WordprocessingDocument doc, int commentId)
{
var mainPart = doc.MainDocumentPart!;
// Need to find the paraId for this commentId, then update commentsExtended.xml
// Step 1: Get paraId from comments.xml
var commentsPart = mainPart.WordprocessingCommentsPart!;
var comment = commentsPart.Comments!
.Elements()
.FirstOrDefault(c => c.Id?.Value == commentId.ToString());
// Find the paragraph and get its paraId
string? paraId = null;
if (comment != null)
{
var para = comment.Elements().FirstOrDefault();
var paraIdElem = para?.ParagraphProperties?
.Elements<乳啜攠嘶嘐呓顾纨asiId>().FirstOrDefault();
paraId = paraIdElem?.Val?.Value;
}
if (paraId == null) return;
// Step 2: Update commentsExtended.xml
var commentsExPart = mainPart.WordprocessingCommentsExPart!;
var commentEx = commentsExPart.CommentsEx!
.Elements()
.FirstOrDefault(ce => ce.ParaId?.Value == paraId);
if (commentEx != null)
commentEx.Done = new OnOffValue(true); // Sets done="1"
commentsExPart.CommentsEx!.Save();
}
```
### 5.4 Deleting a Comment (All 4 Files)
```csharp
// Must remove from all 4 files AND from document.xml
public static void DeleteComment(WordprocessingDocument doc, int commentId)
{
var mainPart = doc.MainDocumentPart!;
string commentIdStr = commentId.ToString();
// ── Remove from comments.xml ──
var commentsPart = mainPart.WordprocessingCommentsPart;
if (commentsPart?.Comments != null)
{
var comment = commentsPart.Comments
.Elements()
.FirstOrDefault(c => c.Id?.Value == commentIdStr);
if (comment != null)
{
// Get paraId before deletion for other files
string? paraId = null;
var para = comment.Elements().FirstOrDefault();
var paraIdElem = para?.ParagraphProperties?
.Elements<乳啜攠嘶嘐呓顾纨asiId>().FirstOrDefault();
paraId = paraIdElem?.Val?.Value;
comment.Remove();
// ── Remove from commentsExtended.xml ──
var commentsExPart = mainPart.WordprocessingCommentsExPart;
if (commentsExPart?.CommentsEx != null && paraId != null)
{
var commentEx = commentsExPart.CommentsEx
.Elements()
.FirstOrDefault(ce => ce.ParaId?.Value == paraId);
commentEx?.Remove();
commentsExPart.CommentsEx.Save();
}
// ── Remove from commentsIds.xml ──
var commentsIdsPart = mainPart.WordprocessingCommentsIdsPart;
if (commentsIdsPart?.CommentsIds != null && paraId != null)
{
var cidEntry = commentsIdsPart.CommentsIds
.Elements()
.FirstOrDefault(ci => ci.ParaId?.Value == paraId);
cidEntry?.Remove();
commentsIdsPart.CommentsIds.Save();
}
// ── Remove from commentsExtensible.xml ──
// Need to look up by durableId...
var commentsExtPart = mainPart.WordprocessingCommentsExtensiblePart;
if (commentsExtPart?.CommentsExtensible != null)
{
// Find by matching durableId (must track separately)
var extEntry = commentsExtPart.CommentsExtensible
.Elements()
.FirstOrDefault(); // Match by durableId lookup
extEntry?.Remove();
commentsExtPart.CommentsExtensible.Save();
}
}
}
// ── Remove from document.xml ──
var body = mainPart.Document!.Body!;
// Remove CommentRangeStart
var rangeStart = body.Descendants()
.FirstOrDefault(crs => crs.Id?.Value == commentIdStr);
rangeStart?.Remove();
// Remove CommentRangeEnd
var rangeEnd = body.Descendants()
.FirstOrDefault(cre => cre.Id?.Value == commentIdStr);
rangeEnd?.Remove();
// Remove CommentReference run (the superscript marker)
var commentRefs = body.Descendants()
.Where(cr => cr.Id?.Value == commentIdStr)
.ToList();
foreach (var cr in commentRefs)
{
var run = cr.Parent as Run;
cr.Remove();
run?.Remove();
}
commentsPart?.Comments?.Save();
}
```
---
## 6. Images — Deep Dive
### 6.1 Adding an ImagePart (All Image Types)
```csharp
// All image types supported by AddImagePart:
void AddImageExamples(MainDocumentPart mainPart, string pngPath, string jpegPath,
string gifPath, string svgPath, string bmpPath, string tiffPath)
{
// PNG
var pngPart = mainPart.AddImagePart(ImagePartType.Png);
using (var s = File.OpenRead(pngPath)) pngPart.FeedData(s);
string pngRelId = mainPart.GetIdOfPart(pngPart);
// JPEG
var jpegPart = mainPart.AddImagePart(ImagePartType.Jpeg);
using (var s = File.OpenRead(jpegPath)) jpegPart.FeedData(s);
string jpegRelId = mainPart.GetIdOfPart(jpegPart);
// GIF
var gifPart = mainPart.AddImagePart(ImagePartType.Gif);
using (var s = File.OpenRead(gifPath)) gifPart.FeedData(s);
string gifRelId = mainPart.GetIdOfPart(gifPart);
// SVG (may require additional handling for fallback)
var svgPart = mainPart.AddImagePart(ImagePartType.Svg);
using (var s = File.OpenRead(svgPath)) svgPart.FeedData(s);
string svgRelId = mainPart.GetIdOfPart(svgPart);
// BMP (stored internally as PNG in OOXML)
var bmpPart = mainPart.AddImagePart(ImagePartType.Bmp);
using (var s = File.OpenRead(bmpPath)) bmpPart.FeedData(s);
string bmpRelId = mainPart.GetIdOfPart(bmpPart);
// TIFF (similarly converted)
var tiffPart = mainPart.AddImagePart(ImagePartType.Tiff);
using (var s = File.OpenRead(tiffPath)) tiffPart.FeedData(s);
string tiffRelId = mainPart.GetIdOfPart(tiffPart);
// Also available: ImagePartType.Icon, ImagePartType.Emf, ImagePartType.Wmf
}
```
### 6.2 Inline Image (DW.Inline)
Inline images are anchored to a specific character position, not floating.
```csharp
// Dimensions: widthPx * 9525 EMU = EMU width, heightPx * 9525 EMU = EMU height
// Assuming 600x400 pixel image at 96dpi:
// cx = 600 * 9525 = 5715000 EMU
// cy = 400 * 9525 = 3810000 EMU
long cx = (long)(widthInches * 914400); // From inches to EMU
long cy = (long)(heightInches * 914400); // From inches to EMU
// Or from pixels at 96dpi:
long cxPx = 600, cyPx = 400;
long cx = cxPx * 9525L; // 5715000 EMU
long cy = cyPx * 9525L; // 3810000 EMU
// Drawing → DW.Inline → A.Graphic → A.GraphicData → PIC.Picture
var drawing = new Drawing(
new DW.Inline(
// Extent: defines the image's display size in EMU
new DW.Extent { Cx = cx, Cy = cy },
// EffectExtent: needed for some effects (set to 0 for basic images)
new DW.EffectExtent { EffectExtentL = 0, EffectExtentT = 0, EffectExtentR = 0, EffectExtentB = 0 },
// DocProperties: metadata for the image (Id must be unique in document)
new DW.DocProperties { Id = 1U, Name = "Image_1", Description = "A sample image" },
// NonVisualGraphicFrameDrawingProperties: locks and frame settings
new DW.NonVisualGraphicFrameDrawingProperties(
new A.GraphicFrameLocks { NoChangeAspect = true }
),
// The actual image
new A.Graphic(
new A.GraphicData(
new PIC.Picture(
// Non-visual properties
new PIC.NonVisualPictureProperties(
new PIC.NonVisualDrawingProperties { Id = 0U, Name = "image1" },
new PIC.NonVisualPictureDrawingProperties()
),
// Fill: how the image is stretched to fill its frame
new PIC.BlipFill(
// Blip: the actual image data reference
new A.Blip { Embed = relId, CompressionState = A.BlipCompressionValues.Print },
// Stretch: how to fill if aspect ratio doesn't match
new A.Stretch(new A.FillRectangle())
),
// ShapeProperties: transform and geometry
new PIC.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = cx, Cy = cy }
),
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
}
);
// Append to a paragraph
var para = new Paragraph(new Run(drawing));
body.Append(para);
```
### 6.3 Floating / Anchored Image (DW.Anchor)
Floating images have text wrapping and can be positioned relative to page, margin, column, or paragraph.
```csharp
// DW.Anchor — positioned floating image with text wrapping
// Key differences from Inline:
// - DW.Anchor instead of DW.Inline
// - DW.PositionH / DW.PositionV for positioning
// - wrapping element (WrapSquare, WrapTight, etc.)
// - can have extent on the anchor (effect extent)
var floatingDrawing = new Drawing(
new DW.Anchor(
// Horizontal positioning
new DW.SimplePosition { X = 0L, Y = 0L }, // Offset from anchor point
new DW.HorizontalPosition(
new DW.PositionOffset((914400L * 2).ToString()) // 2 inches from left
)
{ RelativeFrom = DW.HorizontalRelativePositionValues.Page },
// Vertical positioning
new DW.VerticalPosition(
new DW.PositionOffset((914400L * 3).ToString()) // 3 inches from top
)
{ RelativeFrom = DW.VerticalRelativePositionValues.Page },
// Image extent (size)
new DW.Extent { Cx = cx, Cy = cy },
new DW.EffectExtent { EffectExtentL = 0, EffectExtentT = 0, EffectExtentR = 0, EffectExtentB = 0 },
new DW.DocProperties { Id = 2U, Name = "Floating_Image" },
new DW.NonVisualGraphicFrameDrawingProperties(
new A.GraphicFrameLocks { NoChangeAspect = true }
),
// Text wrapping — several options:
// WrapSquare: text wraps on all sides (default)
// WrapTight: text wraps close to image shape
// WrapThrough: text wraps through the image
// WrapTopAndBottom: image on its own line, text above and below
// WrapNone: image behind/in front of text
new DW.WrapSquare { WrapText = DW.WrapTextValues.RightMargin },
// Layout in table cell (if applicable)
new DW.DocPart Gallery { Val = DW.DocPartGalleryValues.Default },
// Change paragraph that the image is anchored to
// Allow the image to move with the paragraph
new DW.EditingIndependentFromParagraph { Val = false },
// Horizontal anchor: anchor to character/column/margin/page
new DW.HorizontalAnchor { Val = DW.HorizontalAnchorValues.Page },
// Vertical anchor: anchor to character/line/margin/page/paragraph
new DW.VerticalAnchor { Val = DW.VerticalAnchorValues.Page },
// Alignment (if using alignment-based positioning)
new DW.Aligned { Horizontal = DW.HorizontalAlignmentValues.Left,
Vertical = DW.VerticalAlignmentValues.Top },
new A.Graphic(...)
)
{
// Anchor lock: prevents moving in Word UI
EditId = "1A2B3C",
BehindDoc = false, // true = behind text, false = in front
Locked = false,
LayoutInCell = true, // Allow layout inside table cells
AllowOverlap = true // Allow overlap with other floating elements
}
);
```
### 6.4 Text Wrapping Options
```csharp
// WrapSquare — text surrounds on all sides
new DW.WrapSquare { WrapText = DW.WrapTextValues.RightMargin }
// WrapTight — text follows contour of image (if shape has custom geometry)
new DW.WrapTight { WrapText = DW.WrapTextValues.LeftMargin }
// WrapThrough — text intermingles with image
new DW.WrapThrough(
new DW.WrapTextValues.LeftMargin, // Text on left
new DW.WrapTextValues.RightMargin // Text on right
)
// WrapTopAndBottom — image on own line
new DW.WrapTopAndBottom()
// WrapNone — image is at anchor position, text overlays (or vice versa)
new DW.WrapNone()
// Behind document text:
var anchor = new DW.Anchor(...){ BehindDoc = true, Locked = false };
```
### 6.5 Image Sizing — EMU Calculations
```csharp
// EMU reference:
// 1 inch = 914400 EMU
// 1 cm = 360000 EMU
// 1 pixel at 96dpi = 9525 EMU
// 1 pixel at 72dpi = 635 EMU (point, not EMU)
public static class ImageSizing
{
// From pixel dimensions at given DPI
public static (long cx, long cy) FromPixels(int widthPx, int heightPx, int dpi = 96)
{
long emuPerPixel = 914400L / dpi; // ~9525 at 96dpi
return (widthPx * emuPerPixel, heightPx * emuPerPixel);
}
// From inches
public static (long cx, long cy) FromInches(double widthIn, double heightIn)
{
return ((long)(widthIn * 914400), (long)(heightIn * 914400));
}
// From centimeters
public static (long cx, long cy) FromCentimeters(double widthCm, double heightCm)
{
return ((long)(widthCm * 360000), (long)(heightCm * 360000));
}
// Maintain aspect ratio given a target width
public static (long cx, long cy) ScaleToWidth(long originalCx, long originalCy, long targetCx)
{
double ratio = (double)originalCy / originalCx;
return (targetCx, (long)(targetCx * ratio));
}
// Common photo sizes in inches: 4x6, 5x7, 8x10
public static (long cx, long cy) PhotoSize4x6()
=> FromInches(4, 6);
public static (long cx, long cy) PhotoSize5x7()
=> FromInches(5, 7);
public static (long cx, long cy) PhotoSize8x10()
=> FromInches(8, 10);
}
```
### 6.6 Image with Border
```csharp
// Add border to image via PIC.ShapeProperties → A.Outline
new PIC.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = cx, Cy = cy }
),
new A.PresetGeometry(new A.AdjustValueList())
{ Preset = A.ShapeTypeValues.Rectangle },
// The border/outline
new A.Outline(
new A.SolidFill(new A.RgbColorModelHex { Val = "000000" }),
new A.PresetDash { Val = A.PresetLineDashValues.Solid }
)
{ Width = 12700 } // 12700 EMU = 1pt, so 25400 = 2pt
);
```
### 6.7 Image with Alt Text (DocProperties.Description)
```csharp
// Alt text is set via DocProperties.Description
// Also accessible via Picture's alternative text in Word UI
new DW.DocProperties
{
Id = 1U,
Name = "Chart showing growth",
Description = "Bar chart showing quarterly revenue growth from Q1 to Q4 2025"
// Title is also available but Description is what Word shows as alt text
};
// Also set via A.Descriptive (for some image types)
```
### 6.8 Image in Header / Footer
```csharp
// Images in headers/footers work the same as in body, just on the respective part
var headerPart = mainPart.AddNewPart();
// Logo in header
var logoDrawing = new Drawing(
new DW.Inline(
new DW.Extent { Cx = 914400L, Cy = 457200L }, // 1" x 0.5" logo
new DW.EffectExtent(),
new DW.DocProperties { Id = 1U, Name = "HeaderLogo" },
new DW.NonVisualGraphicFrameDrawingProperties(
new A.GraphicFrameLocks { NoChangeAspect = true }),
new A.Graphic(
new A.GraphicData(
new PIC.Picture(
new PIC.NonVisualPictureProperties(
new PIC.NonVisualDrawingProperties { Id = 0U, Name = "logo" },
new PIC.NonVisualPictureDrawingProperties()),
new PIC.BlipFill(
new A.Blip { Embed = headerLogoRelId },
new A.Stretch(new A.FillRectangle())),
new PIC.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = 914400L, Cy = 457200L }),
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 ParagraphProperties(
new Justification { Val = JustificationValues.Right }),
new Run(logoDrawing)
)
);
```
### 6.9 Image in Table Cell
```csharp
// Images in table cells use the same patterns
// With inline: works fine within cell
// With floating/anchor: set LayoutInCell = true
var cellWithImage = new TableCell(
new TableCellProperties(
new TableCellWidth { Width = 2000, Type = TableWidthUnitValues.Dxa }
),
new Paragraph(
new Run(
new Drawing(
new DW.Inline(
new DW.Extent { Cx = 914400L, Cy = 914400L }, // 1"x1"
new DW.EffectExtent(),
new DW.DocProperties { Id = 5U, Name = "CellImage" },
new DW.NonVisualGraphicFrameDrawingProperties(
new A.GraphicFrameLocks { NoChangeAspect = true }),
new A.Graphic(
new A.GraphicData(
new PIC.Picture(...)
) { Uri = "..." }
)
)
{ DistanceFromTop = 0U, DistanceFromBottom = 0U,
DistanceFromLeft = 0U, DistanceFromRight = 0U }
)
)
)
);
```
### 6.10 Replacing an Image (Update Blip.Embed)
```csharp
// To replace an existing image, update the Blip's Embed relationship ID
// 1. Get the existing image's relationship ID from Blip.Embed
// 2. Replace the image data in that ImagePart with new data
// 3. Keep the same relationship ID (so all references remain valid)
public static void ReplaceImage(WordprocessingDocument doc, string newImagePath)
{
var mainPart = doc.MainDocumentPart!;
var body = mainPart.Document!.Body!;
foreach (var drawing in body.Descendants())
{
// Look for inline or anchor images
var inline = drawing.Descendants().FirstOrDefault();
var anchor = drawing.Descendants().FirstOrDefault();
var blipFill = (inline ?? anchor as OpenXmlElement)?
.Descendants().FirstOrDefault();
if (blipFill == null) continue;
var blip = blipFill.Blip;
if (blip?.Embed == null) continue;
string relId = blip.Embed.Value!;
// Get the existing ImagePart
if (mainPart.GetPartById(relId) is ImagePart existingImagePart)
{
// Replace the data
using (var newData = File.OpenRead(newImagePath))
existingImagePart.FeedData(newData);
return; // Replace first found, or loop for all
}
}
}
```
### 6.11 SVG with PNG Fallback (SvgBlip)
```csharp
// SVG images use SvgBlip for modern Word apps, with PNG fallback for older versions
// This is handled through the package structure — Word picks the best supported format
// SVG stored as ImagePartType.Svg, but rendered via BlipFill with extension:
//
//
//
//
//
//
//
var svgImagePart = mainPart.AddImagePart(ImagePartType.Svg);
using (var s = File.OpenRead("chart.svg")) svgImagePart.FeedData(s);
string svgRelId = mainPart.GetIdOfPart(svgImagePart);
// Word automatically handles SVG→PNG fallback in older versions
// No explicit fallback needed in code — the document format handles it
// Note: SvgBlip class in SDK 3.x provides direct support
new A.SvgBlip { Embed = svgRelId };
```
---
## 7. Drawing Shapes (Non-Image)
### 7.1 WordprocessingShape — Basic Shapes (wsp)
WordprocessingShape uses the `wps` namespace for Word's built-in shape library.
```csharp
// Shapes require:
// - MainDocumentPart.AddNewPart() or
// - Embedded via DrawingML inside a Drawing element
// The most common approach is embedding shapes directly in a Drawing element
// Shapes in WordprocessingDrawing are placed like images (inline or anchored)
var shapeDrawing = new Drawing(
new DW.Inline(
new DW.Extent { Cx = 1714500L, Cy = 914400L }, // 1.875" x 1" rectangle
new DW.EffectExtent(),
new DW.DocProperties { Id = 10U, Name = "Rectangle 1" },
new DW.NonVisualGraphicFrameDrawingProperties(
new A.GraphicFrameLocks()),
// The shape itself
new A.Graphic(
new A.GraphicData(
// WordprocessingShape = wsp:wsp (rectangle, roundedRect, ellipse, etc.)
new WSP.WordprocessingShape(
// Non-visual properties
new WSP.NonVisualDrawingShapeProperties(
new A.ShapeLocks { NoChangeAspect = true }
),
// Shape properties (fill, outline, geometry)
new WSP.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = 1714500L, Cy = 914400L }
),
// PresetGeometry determines the shape type
new A.PresetGeometry(new A.AdjustValueList())
{
Preset = A.ShapeTypeValues.Rectangle
// Other values: RoundedRectangle, Ellipse, Triangle, etc.
},
// Fill color (solid)
new A.SolidFill(
new A.RgbColorModelHex { Val = "4472C4" }
),
// Outline
new A.Outline(
new A.NoFill() // No outline
// Or: new A.SolidFill(new A.RgbColorModelHex { Val = "000000" })
// { Width = 12700 } for 1pt border
)
),
// Text box content
new WSP.TextBoxInfo2(
new TextBoxContent(
new Paragraph(
new ParagraphProperties(
new Justification { Val = JustificationValues.Center }),
new Run(
new RunProperties(
new Color { Val = "FFFFFF" },
new Bold()),
new Text("Hello World"))
)
)
)
)
) { Uri = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape" }
)
)
{ DistanceFromTop = 0U, DistanceFromBottom = 0U,
DistanceFromLeft = 0U, DistanceFromRight = 0U }
);
```
**Preset shape types (`A.ShapeTypeValues`):**
- `Rectangle`, `RoundedRectangle`, `Ellipse`, `Triangle`, `RightTriangle`
- `Parallelogram`, `Trapezoid`, `Pentagon`, `Hexagon`, `Octagon`
- `Star4`, `Star5`, `Star6`, `Star8`, `Star10`, `Star12`
- `Heart`, `ArrowRight`, `ArrowLeft`, `ArrowUp`, `ArrowDown`
- `Callout1`, `Callout2`, `Callout3` (with tail)
- `FlowChartProcess`, `FlowChartDecision`, `FlowChartDocument`
### 7.2 Shape with Gradient Fill
```csharp
new WSP.ShapeProperties(
new A.Transform2D(...),
new A.PresetGeometry(new A.AdjustValueList())
{ Preset = A.ShapeTypeValues.RoundedRectangle },
// Gradient fill
new A.GradientFill(
new A.LinearGradientFill(
new A.Stop { Offset = "0", Color = new A.RgbColorModelHex { Val = "4472C4" } },
new A.Stop { Offset = "100000", Color = new A.RgbColorModelHex { Val = "2F5496" } }
)
{ Rotation = 5400000 } // 54° = diagonal
)
// OR: A.RadialGradientFill for radial gradient
);
```
### 7.3 Shape Positioning (Anchored)
```csharp
// Anchored (floating) shapes use DW.Anchor with the shape inside
var anchoredShape = new Drawing(
new DW.Anchor(
new DW.SimplePosition { X = 0L, Y = 0L },
new DW.HorizontalPosition(
new DW.PositionOffset((914400L * 1).ToString())) // 1 inch from left
{ RelativeFrom = DW.HorizontalRelativePositionValues.Margin },
new DW.VerticalPosition(
new DW.PositionOffset((914400L * 2).ToString())) // 2 inches from top
{ RelativeFrom = DW.VerticalRelativePositionValues.Page },
new DW.Extent { Cx = 914400L, Cy = 914400L },
new DW.EffectExtent(),
new DW.DocProperties { Id = 11U, Name = "AnchoredShape" },
new DW.NonVisualGraphicFrameDrawingProperties(new A.GraphicFrameLocks()),
new DW.WrapSquare(),
new A.Graphic(new A.GraphicData(
new WSP.WordprocessingShape(...)
) { Uri = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape" })
)
{ BehindDoc = false, LayoutInCell = true }
);
```
### 7.4 Grouped Shapes (GroupShape)
```csharp
// GroupShape combines multiple shapes into one manipulable unit
// Uses a different URI: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroupShape"
var groupShapeDrawing = new Drawing(
new DW.Inline(
new DW.Extent { Cx = 4572000L, Cy = 2286000L }, // 5" x 2.5"
new DW.EffectExtent(),
new DW.DocProperties { Id = 12U, Name = "Shape Group" },
new DW.NonVisualGraphicFrameDrawingProperties(new A.GraphicFrameLocks()),
new A.Graphic(
new A.GraphicData(
new WPG.GroupShape(
// Child shapes are positioned relative to group origin
// Shape 1 at (0,0)
new WSP.WordprocessingShape(
new WSP.NonVisualDrawingShapeProperties(
new A.ShapeLocks { NoChangeAspect = true }),
new WSP.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = 914400L, Cy = 914400L }),
new A.PresetGeometry(new A.AdjustValueList())
{ Preset = A.ShapeTypeValues.Ellipse },
new A.SolidFill(new A.RgbColorModelHex { Val = "FF0000" })),
new WSP.TextBoxInfo2(
new TextBoxContent(new Paragraph(
new Run(new Text("Red Circle")))))
),
// Shape 2 offset to the right
new WSP.WordprocessingShape(
new WSP.NonVisualDrawingShapeProperties(
new A.ShapeLocks { NoChangeAspect = true }),
new WSP.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 914400L, Y = 0L }, // 1" to the right
new A.Extents { Cx = 914400L, Cy = 914400L }),
new A.PresetGeometry(new A.AdjustValueList())
{ Preset = A.ShapeTypeValues.Rectangle },
new A.SolidFill(new A.RgbColorModelHex { Val = "00FF00" })),
new WSP.TextBoxInfo2(
new TextBoxContent(new Paragraph(
new Run(new Text("Green Square")))))
)
)
) { Uri = "http://schemas.microsoft.com/office/word/2010/wordprocessingGroupShape" }
)
)
{ DistanceFromTop = 0U, DistanceFromBottom = 0U,
DistanceFromLeft = 0U, DistanceFromRight = 0U }
);
```
### 7.5 Shape Effects — Shadow, Reflection
```csharp
// Shadow effect
new WSP.ShapeProperties(
new A.Transform2D(...),
new A.PresetGeometry(new A.AdjustValueList())
{ Preset = A.ShapeTypeValues.RoundedRectangle },
new A.SolidFill(new A.RgbColorModelHex { Val = "4472C4" }),
// Shadow via EffectList
new A.EffectList(
new A.OuterShadow(
new A.RgbColorModelHex { Val = "000000" }
)
{
BlurRadius = 50800L, // 4pt blur (50800 EMU = 4pt at 12700EMU/pt)
Distance = 38100L, // 3pt offset
Direction = 2700000, // 45° (in 60000ths of a degree)
Alignment = A.RectangleAlignmentValues.BottomRight
}
)
);
// Reflection
new A.EffectList(
new A.Reflection(
new A.ReflectionEffect()
{
ReflectionBlurRadius = 63500L, // 5pt
ReflectionDistance = 76200L, // 6pt
ReflectionFade = 50000, // 50% fade
ReflectionOverlap = 25000 // 25% overlap
}
)
);
```
---
## 8. Math / Equations (OMML)
### 8.1 OfficeMath Container — Basic Setup
```csharp
// All math equations must be inside an OfficeMath container
// OfficeMath can be inline (in a run) or display (in its own paragraph)
// Inline equation in a run
var inlineMathPara = new Paragraph(
new Run(
new RunProperties(new RunFonts { Ascii = "Cambria Math", HighAnsi = "Cambria Math" }),
// Inline math: use OfficeMath directly in Run
new OfficeMath(
new M.Fraction(
new M.Numerator(
new M.Run(new M.Text("1"))
),
new M.Denominator(
new M.Run(new M.Text("2"))
)
)
)
)
);
// Display equation (on its own centered paragraph)
var displayMathPara = new Paragraph(
new ParagraphProperties(
new Justification { Val = JustificationValues.Center }
),
new Run(
new OfficeMath(
new M.Fraction(
new M.Numerator(
new M.Run(new M.Text("x"))
),
new M.Denominator(
new M.Run(new M.Text("y"))
)
)
)
)
);
// To make a display equation centered with extra spacing:
var displayEquationPara = new Paragraph(
new ParagraphProperties(
new SpacingBetweenLines { Before = "240", After = "240" },
new Justification { Val = JustificationValues.Center }
),
new Run(new OfficeMath(
// Equation content here
))
);
```
### 8.2 Fraction (M.Fraction)
```csharp
// \frac{x}{y} pattern
new M.Fraction(
new M.Numerator(
new M.Run(
new M.RunText("x") { Space = SpaceProcessingModeValues.Preserve }
)
),
new M.Denominator(
new M.Run(
new M.RunText("y") { Space = SpaceProcessingModeValues.Preserve }
)
)
);
// Nested fraction: (a+b)/(c+d)
new M.Fraction(
new M.Numerator(
new M.Run(new M.Text("a")) { FontSize = 24 },
new M.Run(new M.Text("+")) { FontSize = 24 },
new M.Run(new M.Text("b")) { FontSize = 24 }
),
new M.Denominator(
new M.Run(new M.Text("c")) { FontSize = 24 },
new M.Run(new M.Text("+")) { FontSize = 24 },
new M.Run(new M.Text("d")) { FontSize = 24 }
)
);
// Display fraction (skips 1 as numerator/denominator style)
new M.Fraction(
new M.Numerator(...),
new M.Denominator(...),
new M.FractionPr(
new M.Type { Val = M.FractionValues.Skewed } // or Normal, Linear,丝
)
);
```
### 8.3 Superscript and Subscript
```csharp
// Superscript: x²
new M.Superscript(
new M.Base(
new M.Run(new M.Text("x")))
),
new M.SuperscriptOperand(
new M.Run(new M.Text("2")))
)
);
// Subscript: x₁
new M.Subscript(
new M.Base(
new M.Run(new M.Text("x")))
),
new M.SubscriptOperand(
new M.Run(new M.Text("1")))
)
);
// Pre-sub/superscript: _b^a (baseline then super)
// or use M.SubscriptSuperscript for combined
// SubscriptSuperscript (both at once): _b^a C
new M.SubscriptSuperscript(
new M.Base(new M.Run(new M.Text("C"))),
new M.Subscript(new M.Run(new M.Text("b"))),
new M.Superscript(new M.Run(new M.Text("a")))
);
```
### 8.4 Square Root and Nth Root
```csharp
// Square root: √x
new M.Radical(
new M.Root(
new M.Run(new M.Text("x")))
)
);
// Square root with degree hidden (just √)
new M.Radical(
new M.Root(
new M.Run(new M.Text("x")))
),
new M.RadicalPr(
new M.Degree { Val = false } // Hide the root index
)
);
// Nth root: ∛(x+1) — cube root of (x+1)
new M.Radical(
new M.Root(
new M.Fraction(
new M.Numerator(new M.Run(new M.Text("1"))),
new M.Denominator(new M.Run(new M.Text("3")))
)
), // This is the "3" for cube root
new M.Root(
new M.Run(new M.Text("x"))),
new M.Run(new M.Text("+"))),
new M.Run(new M.Text("1")))
)
);
// Actually, for nth root: first Root is the index (degree), second is the radicand
new M.Radical(
new M.Root(new M.Run(new M.Text("3"))), // The index: 3rd root
new M.Root(
new M.Run(new M.Text("x")),
new M.Run(new M.Text("+")),
new M.Run(new M.Text("1"))
)
);
```
### 8.5 N-ary Operators — Integral, Summation, Product
```csharp
// Integral ∫ from a to b of f(x) dx
new M.Nary(
new M.NaryProperties(
new M.NaryType { Val = M.NaryValues.Integral } // ∫
)
{
SubSuperscript = M.SubSuperscriptValues.NoSubSuperscript
},
new M.Base(
new M.Run(new M.Text("f(x)")))
),
new M.Subscript(
new M.Run(new M.Text("a")))
),
new M.Superscript(
new M.Run(new M.Text("b")))
)
);
// Summation Σ from i=1 to n of i²
new M.Nary(
new M.NaryProperties(
new M.NaryType { Val = M.NaryValues.Sum }, // Σ
new M.GrowBindings = true
),
new M.Base(
new M.SubscriptSuperscript(
new M.Base(new M.Run(new M.Text("i"))),
new M.Subscript(new M.Run(new M.Text("1"))),
new M.Superscript(new M.Run(new M.Text("n")))
)
),
new M.Subscript(
new M.Run(new M.Text("i")))
),
new M.Superscript(
new M.Run(new M.Text("2")))
)
);
// Product ∏ from i=1 to n
new M.Nary(
new M.NaryProperties(
new M.NaryType { Val = M.NaryValues.Product } // ∏
),
new M.Base(
new M.SubscriptSuperscript(
new M.Base(new M.Run(new M.Text("i"))),
new M.Subscript(new M.Run(new M.Text("1"))),
new M.Superscript(new M.Run(new M.Text("n")))
)
),
new M.Subscript(
new M.Run(new M.Text("i")))
),
new M.Superscript(
new M.Run(new M.Text("2")))
)
);
// N-ary type values: Integral, Sum, Product, Union, Intersection, etc.
```
### 8.6 Matrix
```csharp
// 2x2 matrix
// [a b]
// [c d]
new M.Matrix(
new M.MatrixRows(
// Row 1
new M.MatrixRow(
new M.MatrixCell(
new M.Run(new M.Text("a"))
),
new M.MatrixCell(
new M.Run(new M.Text("b"))
)
),
// Row 2
new M.MatrixRow(
new M.MatrixCell(
new M.Run(new M.Text("c"))
),
new M.MatrixCell(
new M.Run(new M.Text("d"))
)
)
),
new M.MatrixProperties(
new M.Jc { Val = M.JustificationValues.Center }, // Centered
new M.Structure { Val = M.MathStructureValues.SinglePUNCT },
new M.RowSpacing { Val = 120 }, // Row spacing in twips
new M.RowSpacing1 { Val = 120 }
)
)
```
### 8.7 Delimiter (Parentheses/Brackets/Braces)
```csharp
// (a + b) or [a + b] or {a + b}
new M.Delimiter(
new M.DelimiterProperties(
new M.Begin(new M.Text("(")), // Opening char
new M.End(new M.Text(")")), // Closing char
new M.Separator(new M.Text(",")), // Separator between elements
new M.Structure { Val = M.MathStructureValues.Minimal }
),
new M.DelimiterContents(
new M.Run(new M.Text("a")),
new M.Run(new M.Text("+")),
new M.Run(new M.Text("b"))
)
);
// {a, b, c} with curly braces
new M.Delimiter(
new M.DelimiterProperties(
new M.Begin(new M.Text("{")),
new M.End(new M.Text("}")),
new M.Separator(new M.Text(","))
),
new M.DelimiterContents(
new M.Run(new M.Text("a")),
new M.Run(new M.Text("b")),
new M.Run(new M.Text("c"))
)
);
// 2x2 matrix in parentheses
new M.Delimiter(
new M.DelimiterProperties(
new M.Begin(new M.Text("(")),
new M.End(new M.Text(")"))
),
new M.DelimiterContents(
// Inline 2x2 using subscripts
new M.SubscriptSuperscript(...)
)
);
```
### 8.8 Equation Array (Aligned Equations)
```csharp
// EquationArray (M.EquationArray) creates a series of equations aligned at markers
// Like \begin{align} in LaTeX
new M.EquationArray(
new M.EquationArrayProperties(
new M.Jc { Val = M.JustificationValues.Left },
new M.RowSpacing { Val = 240 },
new M.RowSpacing1 { Val = 240 }
),
// Each equation is a Paragraph inside the array
new Paragraph(new Run(new M.Text("x") { Space = SpaceProcessingModeValues.Preserve })),
new Paragraph(
new Run(new M.Text("+") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new M.Text("y") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new M.Text("=") { Space = SpaceProcessingModeValues.Preserve }),
new Run(new M.Text("z") { Space = SpaceProcessingModeValues.Preserve })
)
);
// Or use M.Break with AlignmentTab for manual alignment points
```
### 8.9 Greek Letters and Math Symbols
```csharp
// Greek letters via M.RunText with Symbol font or Unicode
// Common Greek letters and their uses:
// α (alpha)
new M.Run(new M.Text("\u03B1")) // or use Unicode directly
// β (beta)
new M.Run(new M.Text("\u03B2"))
// γ (gamma)
new M.Run(new M.Text("\u03B3"))
// π (pi) — use Greek small letter pi
new M.Run(new M.Text("\u03C0"))
// σ (sigma)
new M.Run(new M.Text("\u03C3"))
// Σ (Sigma, capital) — summation symbol
new M.Run(new M.Text("\u03A3"))
// θ (theta)
new M.Run(new M.Text("\u03B8"))
// ∞ (infinity)
new M.Run(new M.Text("\u221E"))
// ≤ (less than or equal)
new M.Run(new M.Text("\u2264"))
// ≥ (greater than or equal)
new M.Run(new M.Text("\u2265"))
// ≠ (not equal)
new M.Run(new M.Text("\u2260"))
// ± (plus-minus)
new M.Run(new M.Text("\u00B1"))
// × (multiplication)
new M.Run(new M.Text("\u00D7"))
// ÷ (division)
new M.Run(new M.Text("\u00F7"))
// For best results, set the font to "Cambria Math" on math runs
new M.Run(
new RunFonts { Ascii = "Cambria Math", HighAnsi = "Cambria Math" },
new M.Text("\u03C0")
)
```
---
## 9. Numbering System — Deep Dive
### 9.1 Architecture Overview
```
NumberingDefinitionsPart (numbering.xml)
└──
├── (templates)
│ ├── × 9 (levels 0-8)
│ └── links to this abstractNum
└── (instances)
└──
```
**Key rule**: `AbstractNum` must appear BEFORE `NumberingInstance` in the XML root.
### 9.2 AbstractNum with Multi-Level Decimal Numbering
```csharp
// AbstractNum: the numbering template (what it looks like)
// NumberingInstance: a specific use of that template (how it's applied)
var numberingPart = mainPart.AddNewPart();
var numbering = new Numbering();
// ─────────────────────────────────────────────────────────────
// Step 1: Define AbstractNum (the template)
// ─────────────────────────────────────────────────────────────
var abstractNum = new AbstractNum { AbstractNumberId = 1 };
// MultiLevelType specifies this is a multi-level list
abstractNum.Append(new MultiLevelType { Val = MultiLevelValues.Multilevel });
// Level 0: "1." — decimal, bold number
abstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "%1." },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "360", Hanging = "360" } // 0.25" hanging indent
),
new NumberingSymbolRunProperties(
new Bold(),
new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" },
new Color { Val = "2F5496" }
)
) { LevelIndex = 0 });
// Level 1: "1.1." — indent 0.5"
abstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "%1.%2." },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "720", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" }
)
) { LevelIndex = 1 });
// Level 2: "1.1.1."
abstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "%1.%2.%3." },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "1080", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" }
)
) { LevelIndex = 2 });
// ─────────────────────────────────────────────────────────────
// Step 2: Create NumberingInstance (a reference to the template)
// ─────────────────────────────────────────────────────────────
var numInstance = new NumberingInstance(
new AbstractNumId { Val = 1 } // Points to abstractNum above
) { NumberID = 1 };
// ─────────────────────────────────────────────────────────────
// Step 3: Assemble — AbstractNum BEFORE NumberingInstance!
// ─────────────────────────────────────────────────────────────
numbering.Append(abstractNum);
numbering.Append(numInstance);
numberingPart.Numbering = numbering;
numberingPart.Numbering.Save();
// ─────────────────────────────────────────────────────────────
// Step 4: Apply to a paragraph
// ─────────────────────────────────────────────────────────────
var numberedPara = new Paragraph(
new ParagraphProperties(
new NumberingProperties(
new NumberingLevelReference { Val = 0 }, // Use level 0 of numId 1
new NumberingId { Val = 1 } // Use numbering instance 1
)
),
new Run(new Text("First item"))
);
// For level 1 sub-item:
var subItemPara = new Paragraph(
new ParagraphProperties(
new NumberingProperties(
new NumberingLevelReference { Val = 1 }, // Use level 1
new NumberingId { Val = 1 }
)
),
new Run(new Text("Sub-item"))
);
```
### 9.3 Bullet Lists with Custom Symbols
```csharp
// Bullet numbering uses NumberFormatValues.Bullet
// The bullet character is defined in LevelText and NumberingSymbolRunProperties
var bulletAbstractNum = new AbstractNum { AbstractNumberId = 2 };
bulletAbstractNum.Append(new MultiLevelType { Val = MultiLevelValues.Multilevel });
// Level 0 bullet: ● (Unicode bullet)
bulletAbstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Bullet }, // Key: Bullet format
new LevelText { Val = "\u2022" }, // ● bullet character
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "720", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Symbol", HighAnsi = "Symbol" }
// Symbol font maps ● to character 0xD8 in Symbol encoding
)
) { LevelIndex = 0 });
// Level 1 bullet: ○ (white circle)
bulletAbstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Bullet },
new LevelText { Val = "\u25CB" }, // ○ Unicode
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "1080", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Courier New", HighAnsi = "Courier New" }
)
) { LevelIndex = 1 });
// Level 2 bullet: ■ (black square)
bulletAbstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Bullet },
new LevelText { Val = "\u25A0" }, // ■
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "1440", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Arial", HighAnsi = "Arial" }
)
) { LevelIndex = 2 });
var bulletNumInstance = new NumberingInstance(
new AbstractNumId { Val = 2 }
) { NumberID = 2 };
numbering.Append(bulletAbstractNum);
numbering.Append(bulletNumInstance);
```
**Common bullet characters:**
| Symbol | Character | Unicode | Common Font |
|--------|-----------|---------|-------------|
| ● Filled circle | Bullet | U+2022 | Symbol |
| ○ Empty circle | White circle | U+25CB | Arial |
| ■ Filled square | Black square | U+25A0 | Arial |
| □ Empty square | White square | U+25A1 | Arial |
| ➢ Right arrow | Right arrow | U+27A2 | Wingdings |
| ✓ Checkmark | Check mark | U+2713 | Wingdings |
| ✗ Cross | Ballot X | U+2717 | Wingdings |
| ▶ Play | Right triangle | U+25B6 | Arial |
### 9.4 Restart Numbering at Specific Point
```csharp
// Method 1: StartOverride on a specific paragraph
// Use LevelOverride + StartOverride to restart at a specific level
var restartNumInstance = new NumberingInstance(
new AbstractNumId { Val = 1 }
) { NumberID = 3 };
// Override level 0 to start at 5 instead of 1
restartNumInstance.Append(new LevelOverride { LevelIndex = 0 },
new StartOverrideNumberingValue { Val = 5 }
);
// Apply this to a paragraph — this paragraph starts numbering at 5
var restartPara = new Paragraph(
new ParagraphProperties(
new NumberingProperties(
new NumberingLevelReference { Val = 0 },
new NumberingId { Val = 3 } // Use the restart instance
)
),
new Run(new Text("Item 5 (restarted)"))
);
```
### 9.5 Continue Numbering from Previous List
```csharp
// By default, Word continues numbering across lists using the same AbstractNum.
// To force continuation, ensure the list uses the same numId.
// If you need explicit continuation control:
var continuedNumInstance = new NumberingInstance(
new AbstractNumId { Val = 1 }
) { NumberID = 4 };
// When multiple NumberingInstances share the same AbstractNumId,
// they share the same numbering state (continuation)
// To prevent continuation (start fresh), use a new AbstractNum:
var freshAbstractNum = new AbstractNum { AbstractNumberId = 5 };
freshAbstractNum.Append(new MultiLevelType { Val = MultiLevelValues.Multilevel });
// ... define levels ...
var freshNumInstance = new NumberingInstance(
new AbstractNumId { Val = 5 }
) { NumberID = 5 };
// This starts at 1 again, independent of the previous list
```
### 9.6 Link Numbering to Heading Styles (Outline Numbering)
```句话说,link numbering to heading styles so that Heading1 starts a new numbering sequence, Heading2 is a sub-item, etc.
```csharp
// This links styles to numbering levels automatically via StyleLink
var abstractNumForOutline = new AbstractNum { AbstractNumberId = 10 };
abstractNumForOutline.Append(new MultiLevelType { Val = MultiLevelValues.Multilevel });
abstractNumForOutline.Append(new StyleLink { Val = "Heading1" }); // Links level 0 to Heading1
abstractNumForOutline.Append(new StyleLink { Val = "Heading2" }); // Links level 1 to Heading2
abstractNumForOutline.Append(new StyleLink { Val = "Heading3" }); // Links level 2 to Heading3
// Level 0 for Heading1
abstractNumForOutline.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "Chapter %1" },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "360", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new Bold(),
new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" },
new FontSize { Val = "28" }
)
) { LevelIndex = 0 });
// Level 1 for Heading2
abstractNumForOutline.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "%1.%2" },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "720", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" }
)
) { LevelIndex = 1 });
// Level 2 for Heading3
abstractNumForOutline.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "%1.%2.%3" },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "1080", Hanging = "360" }
),
new NumberingSymbolRunProperties(
new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" }
)
) { LevelIndex = 2 });
// Now when you apply Heading1/2/3 styles, numbering follows automatically
var heading1Para = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }),
new Run(new Text("Introduction")) // Automatically gets "Chapter 1" prefix
);
var heading2Para = new Paragraph(
new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }),
new Run(new Text("Background")) // Automatically gets "1.1" prefix
);
```
### 9.7 NumberingFormat Values Reference
```csharp
// NumberFormatValues enum — all supported numbering formats:
NumberFormatValues.Decimal // 1, 2, 3...
NumberFormatValues.LowerRoman // i, ii, iii...
NumberFormatValues.UpperRoman // I, II, III...
NumberFormatValues.LowerLetter // a, b, c...
NumberFormatValues.UpperLetter // A, B, C...
NumberFormatValues.Ordinal // 1st, 2nd, 3rd... (locale-dependent)
NumberFormatValues.OrdinalText // First, Second, Third... (locale-dependent)
NumberFormatValues.Hex // 0, 1, 2... F, 10, 11... (hexadecimal)
NumberFormatValues.ChicagoManual // Chapter numbering (I, A, 1, a)
NumberFormatValues.Kanji // 漢数字
NumberFormatValues.KanjiDigit // 一, 二, 三...
NumberFormatValues.DoubleByte // Ideographic: 一, 二, 三
NumberFormatValues.ArabicFullWidth // Full-width: 1, 2, 3
NumberFormatValues.Bullet // Custom symbol (●, ✓, etc.)
NumberFormatValues.None // No number
```
### 9.8 IsLegalNumberingStyle — Using Arabic with Nested Levels
```csharp
// IsLegalNumberingStyle=false (default): each level can have its own format
// IsLegalNumberingStyle=true: forces all sub-levels to use Arabic numerals
// This is important for legal/formal numbering where you want:
// 1. level 1 = A, B, C (alphabetic)
// 2. level 2 = 1, 2, 3 (Arabic) — NOT a, b, c
// Without IsLegalNumberingStyle=false, level 2 would inherit alphabetic
var legalAbstractNum = new AbstractNum { AbstractNumberId = 20 };
legalAbstractNum.Append(new MultiLevelType { Val = MultiLevelValues.Multilevel });
legalAbstractNum.Append(new IsLegalNumberingStyle()); // No val = true (default when element present)
// Level 0: A. B. C.
legalAbstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.UpperLetter },
new LevelText { Val = "%1." },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "360", Hanging = "360" }
)
) { LevelIndex = 0 });
// Level 1: 1. 2. 3. (NOT a. b. c.)
legalAbstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.Decimal },
new LevelText { Val = "%2." },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "720", Hanging = "360" }
)
) { LevelIndex = 1 });
// Level 2: (a) (b) (c)
legalAbstractNum.Append(new Level(
new StartNumberingValue { Val = 1 },
new NumberingFormat { Val = NumberFormatValues.LowerLetter },
new LevelText { Val = "(%3)" },
new LevelJustification { Val = LevelJustificationValues.Left },
new ParagraphProperties(
new Indentation { Left = "1080", Hanging = "360" }
)
) { LevelIndex = 2 });
```
---
## 10. Document Protection & Encryption
### 10.1 DocumentProtection — Basic Forms
```csharp
// DocumentProtection is placed in DocumentSettingsPart
var settingsPart = mainPart.AddNewPart();
settingsPart.Settings = new Settings();
// ReadOnly: prevents editing, allows reading
settingsPart.Settings.Append(new DocumentProtection
{
Edit = DocumentProtectionValues.ReadOnly,
Enforcement = true
});
// Comments: can only add/edit comments (not modify body text)
settingsPart.Settings.Append(new DocumentProtection
{
Edit = DocumentProtectionValues.Comments,
Enforcement = true
});
// TrackedChanges: can only edit with track changes ON
settingsPart.Settings.Append(new DocumentProtection
{
Edit = DocumentProtectionValues.TrackedChanges,
Enforcement = true
});
// Forms: only form fields are editable
settingsPart.Settings.Append(new DocumentProtection
{
Edit = DocumentProtectionValues.Forms,
Enforcement = true
});
```
### 10.2 Password Hashing for DocumentProtection
Modern Word uses SHA-512 with salt for password hashing (ECMA-376 standard).
```csharp
// SHA-512 password hashing for strong protection
// CryptographicProviderType must be "rsaAES" or "rsaAES" for SHA-512
settingsPart.Settings.Append(new DocumentProtection
{
Edit = DocumentProtectionValues.ReadOnly,
Enforcement = true,
CryptographicProviderType = CryptProviderValues.RsaAES,
CryptographicAlgorithmClass = CryptAlgorithmClassValues.Hash,
CryptographicAlgorithmType = CryptAlgorithmValues.TypeAny,
CryptographicAlgorithmSid = 14, // SHA-512
CryptographicSpinCount = 100000U,
Hash = "base64-encoded-hash-here",
Salt = "base64-encoded-salt-here"
});
// Generate hash in .NET:
public static (string hash, string salt) GeneratePasswordHash(string password, int spinCount = 100000)
{
byte[] saltBytes = new byte[16];
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
rng.GetBytes(saltBytes);
// PBKDF2 with SHA-512, 100000 iterations
using var pbkdf2 = new System.Security.Cryptography.Rfc2898DeriveBytes(
password, saltBytes, spinCount, System.Security.Cryptography.HashAlgorithmName.SHA512);
byte[] hash = pbkdf2.GetBytes(64); // 512 bits
return (Convert.ToBase64String(hash), Convert.ToBase64String(saltBytes));
}
```
### 10.3 WriteProtection — Recommend Opening as Read-Only
```csharp
// WriteProtection is different from DocumentProtection
// It recommends (but doesn't enforce) that users open as read-only
// Found in extended properties (docProps/custom.xml or settings)
settingsPart.Settings.Append(new WriteProtection
{
Recommended = true // "Recommend opening as read-only"
});
// Or force read-only recommendation with a specific application name
settingsPart.Settings.Append(new WriteProtection
{
Recommended = true,
ApplicationName = "Microsoft Word"
});
```
### 10.4 Restrict Editing to Form Fields Only
```csharp
// This protects the document but allows editing in form field content controls
settingsPart.Settings = new Settings(
new DocumentProtection
{
Edit = DocumentProtectionValues.Forms,
Enforcement = true
},
// Also set to allow editing only in form fields
new EditingRestrictions { Val = EditingRestrictionValues.Forms }
);
```
### 10.5 PermStart / PermEnd — Editable Regions in Protected Document
Allow specific regions (ranges) to be edited even when the document is protected.
```csharp
//
// Editable text
//
// Even when document protection is on, this range can be edited by everyone
var editableRegion = new Paragraph(
new ParagraphProperties(
new PermStart { Id = 1, EditorGroup = RangePermissionEditingGroupValues.Everyone }
),
new Run(new Text("This text can be edited even in a protected document.") { Space = SpaceProcessingModeValues.Preserve }),
new ParagraphProperties(
new PermEnd { Id = 1 }
)
);
// EditorGroup values:
// Everyone — anyone can edit
// Administrators — only administrators
// Contributors — only contributors
// Editors — only editors
// Owners — only document owners
// Nobody — nobody can edit (use with w:perm sbz="1" for "does not include")
// Use specific user name:
//
// For tracked changes review scenario (allow comments but not direct editing):
var commentableRegion = new Paragraph(
new ParagraphProperties(
new PermStart { Id = 2, EditorGroup = RangePermissionEditingGroupValues.Everyone }
),
new Run(new Text("This region allows comments and tracked changes.") { Space = SpaceProcessingModeValues.Preserve }),
new ParagraphProperties(
new PermEnd { Id = 2 }
)
);
```
### 10.6 Full Document Protection with Password and Salt
```csharp
public static void ProtectDocument(
WordprocessingDocument doc,
string password,
DocumentProtectionValues protectionType)
{
var settingsPart = doc.MainDocumentPart!.AddNewPart();
if (settingsPart.Settings == null)
settingsPart.Settings = new Settings();
// Generate password hash
byte[] salt = new byte[16];
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
rng.GetBytes(salt);
int spinCount = 100000;
using var pbkdf2 = new System.Security.Cryptography.Rfc2898DeriveBytes(
password, salt, spinCount, System.Security.Cryptography.HashAlgorithmName.SHA512);
byte[] hash = pbkdf2.GetBytes(64);
var protection = new DocumentProtection
{
Edit = protectionType,
Enforcement = true,
CryptographicProviderType = CryptProviderValues.RsaAES,
CryptographicAlgorithmClass = CryptAlgorithmClassValues.Hash,
CryptographicAlgorithmType = CryptAlgorithmValues.TypeAny,
CryptographicAlgorithmSid = 14, // SHA-512
CryptographicSpinCount = (UInt32Value)spinCount,
Hash = Convert.ToBase64String(hash),
Salt = Convert.ToBase64String(salt)
};
settingsPart.Settings.Append(protection);
settingsPart.Settings.Save();
}
```
---
## Quick Reference: Common Element Order in OpenXML
When building complex elements, remember these ordering rules:
### Run Elements Order (inside RunProperties):
`RunFonts` → `Bold`/`Italic` → `Color` → `FontSize` → `Underline` → `VerticalTextAlignment` → `Emphasis` → (any other)
### Paragraph Elements Order (inside ParagraphProperties):
`ParagraphStyleId` → `KeepNext` → `KeepLines` → `PageBreakBefore` → `FrameProperties` → `WidowControl` → `NumPr` → `Indentation` → `SpacingBetweenLines` → `Justification` → `SectionProperties`
### Table Properties Order:
`TableWidth` → `TextDirection` → `Borders` → `Shading` → `TableLayout` → `TableCellMarginDefault`
### SectionProperties Order:
`FootnotePr` → `EndnotePr` → `Type` → `PageSize` → `PageMargin` → `PaperSource` → `PageBorders` → `LineNumberRestart` → `PageNumberFormat` → `TitlePage` → `TextDirection`
---
*Generated for DocumentFormat.OpenXml 3.x / .NET 8+ / C# 12*
*Last updated: 2026-03-22*