3382 lines
119 KiB
Markdown
3382 lines
119 KiB
Markdown
# 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:
|
||
// <w:p>
|
||
// <w:r>
|
||
// <w:fldChar w:fldCharType="begin"/>
|
||
// </w:r>
|
||
// <w:r>
|
||
// <w:instrText xml:space="preserve"> TOC \o "1-3" \h \z \u </w:instrText>
|
||
// </w:r>
|
||
// <w:r>
|
||
// <w:fldChar w:fldCharType="separate"/>
|
||
// </w:r>
|
||
// <w:r>
|
||
// <w:t>Update this field to generate table of contents.</w:t>
|
||
// </w:r>
|
||
// <w:r>
|
||
// <w:fldChar w:fldCharType="end"/>
|
||
// </w:r>
|
||
// </w:p>
|
||
|
||
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<DocumentSettingsPart>();
|
||
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<StyleDefinitionsPart>();
|
||
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<FootnotesPart>();
|
||
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:
|
||
// <w:footnote w:id="3">
|
||
// <w:p>
|
||
// <w:pPr><w:pStyle w:val="FootnoteText"/></w:pPr>
|
||
// <w:r><w:footnoteRef/></w:r>
|
||
// <w:r><w:t xml:space="preserve"> This is the footnote text.</w:t></w:r>
|
||
// </w:p>
|
||
// </w:footnote>
|
||
|
||
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<SectionProperties>().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<EndnotesPart>();
|
||
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
|
||
// <w:fldSimple w:instr=" PAGE "><w:r><w:t>1</w:t></w:r></w:fldSimple>
|
||
new SimpleField(new Run(new Text("1"))) { Instruction = " PAGE " }
|
||
```
|
||
|
||
**Complex Field (Begin/Separate/End)** — full control over each field component:
|
||
```csharp
|
||
// <w:r><w:fldChar w:fldCharType="begin"/></w:r>
|
||
// <w:r><w:instrText> PAGE </w:instrText></w:r>
|
||
// <w:r><w:fldChar w:fldCharType="separate"/></w:r>
|
||
// <w:r><w:t>1</w:t></w:r>
|
||
// <w:r><w:fldChar w:fldCharType="end"/></w:r>
|
||
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<DocumentSettingsPart>();
|
||
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>();
|
||
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<DocumentSettingsPart>();
|
||
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<DocumentSettingsPart>();
|
||
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
|
||
// <w:ins w:id="5" w:author="Alice" w:date="2026-03-22T10:00:00Z">
|
||
// <w:r>
|
||
// <w:t>Inserted text.</w:t>
|
||
// </w:r>
|
||
// </w:ins>
|
||
|
||
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
|
||
// <w:del w:id="6" w:author="Alice" w:date="2026-03-22T10:05:00Z">
|
||
// <w:r>
|
||
// <w:rPr><w:b/></w:rPr>
|
||
// <w:delText>Deleted text.</w:delText>
|
||
// </w:r>
|
||
// </w:del>
|
||
|
||
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 <w:t> inside <w:del> — use <w:delText> 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
|
||
// <w:r>
|
||
// <w:rPr>
|
||
// <w:b/> <!-- New: bold -->
|
||
// <w:rPrChange w:id="7" w:author="Bob" w:date="2026-03-22T11:00:00Z">
|
||
// <w:rPr/> <!-- Old: no formatting -->
|
||
// </w:rPrChange>
|
||
// </w:rPr>
|
||
// <w:t>Formatted text.</w:t>
|
||
// </w:r>
|
||
|
||
// 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
|
||
// <w:pPr>
|
||
// <w:jc w:val="center"/> <!-- New: centered -->
|
||
// <w:pPrChange w:id="8" w:author="Bob" w:date="2026-03-22T11:05:00Z">
|
||
// <w:pPr>
|
||
// <w:jc w:val="left"/> <!-- Old: left-aligned -->
|
||
// </w:pPr>
|
||
// </w:pPrChange>
|
||
// </w:pPr>
|
||
|
||
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
|
||
// <w:p>
|
||
// <w:pPr>
|
||
// <w:pPrChange .../>
|
||
// </w:pPr>
|
||
// <w:r>
|
||
// <w:rPr>
|
||
// <w:b/> <!-- New paragraph mark: bold -->
|
||
// <w:rPrChange w:id="9" ...>
|
||
// <w:rPr/> <!-- Old: no formatting on paragraph mark -->
|
||
// </w:rPrChange>
|
||
// </w:rPr>
|
||
// </w:r>
|
||
// </w:r>
|
||
```
|
||
|
||
### 4.7 Table Revision Marks
|
||
|
||
```csharp
|
||
// TableRowInsertionRevision — a row was inserted
|
||
// <w:trPr>
|
||
// <w:ins w:id="10" w:author="Alice" w:date="..."/>
|
||
// </w:trPr>
|
||
|
||
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
|
||
// <w:sectPr>
|
||
// <w:sectPrChange w:id="12" w:author="Bob" w:date="...">
|
||
// <w:sectPr>
|
||
// <w:pgSz w:w="12240" w:h="15840"/> <!-- Old: Letter -->
|
||
// </w:sectPr>
|
||
// </w:sectPrChange>
|
||
// <w:pgSz w:w="16838" w:h="11906"/> <!-- New: A4 -->
|
||
// </w:sectPr>
|
||
|
||
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
|
||
// <w:numPr>
|
||
// <w:ilvl w:val="0"/>
|
||
// <w:numId w:val="3"/>
|
||
// <w:numPrChange w:id="13" w:author="Alice" w:date="...">
|
||
// <w:numPr>
|
||
// <w:ilvl w:val="0"/>
|
||
// <w:numId w:val="1"/> <!-- Old: was numId 1 -->
|
||
// </w:numPr>
|
||
// </w:numPrChange>
|
||
// </w:numPr>
|
||
|
||
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<InsertedRun>().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<DeletedRun>().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<InsertedRun>().ToList();
|
||
foreach (var ins in insertions)
|
||
ins.Remove();
|
||
|
||
// Reject deletions: unwrap w:del, convert w:delText back to w:t
|
||
var deletions = body.Descendants<DeletedRun>().ToList();
|
||
foreach (var del in deletions)
|
||
{
|
||
var parent = del.Parent;
|
||
if (parent == null) continue;
|
||
foreach (var run in del.Elements<Run>().ToList())
|
||
{
|
||
foreach (var delText in run.Elements<DeletedText>().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
|
||
|
||
// <w:moveFrom w:id="14" w:author="Alice" w:date="...">
|
||
// <w:r><w:t>Text that was moved.</w:t></w:r>
|
||
// </w:moveFrom>
|
||
|
||
// At destination:
|
||
// <w:moveTo w:id="14" w:author="Alice" w:date="...">
|
||
// <w:r><w:t>Text that was moved.</w:t></w:r>
|
||
// </w:moveTo>
|
||
|
||
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<OpenXmlElement>())
|
||
{
|
||
// 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<OpenXmlElement>())
|
||
{
|
||
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<WordprocessingCommentsPart>();
|
||
|
||
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<WordprocessingCommentsExPart>();
|
||
|
||
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<WordprocessingCommentsIdsPart>();
|
||
|
||
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<WordprocessingCommentsExtensiblePart>();
|
||
|
||
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<Comment>())
|
||
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<CommentEx>()
|
||
.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<Comment>()
|
||
.FirstOrDefault(c => c.Id?.Value == commentId.ToString());
|
||
|
||
// Find the paragraph and get its paraId
|
||
string? paraId = null;
|
||
if (comment != null)
|
||
{
|
||
var para = comment.Elements<Paragraph>().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<CommentEx>()
|
||
.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<Comment>()
|
||
.FirstOrDefault(c => c.Id?.Value == commentIdStr);
|
||
if (comment != null)
|
||
{
|
||
// Get paraId before deletion for other files
|
||
string? paraId = null;
|
||
var para = comment.Elements<Paragraph>().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<CommentEx>()
|
||
.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<CommentId>()
|
||
.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<CommentExtensible>()
|
||
.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<CommentRangeStart>()
|
||
.FirstOrDefault(crs => crs.Id?.Value == commentIdStr);
|
||
rangeStart?.Remove();
|
||
|
||
// Remove CommentRangeEnd
|
||
var rangeEnd = body.Descendants<CommentRangeEnd>()
|
||
.FirstOrDefault(cre => cre.Id?.Value == commentIdStr);
|
||
rangeEnd?.Remove();
|
||
|
||
// Remove CommentReference run (the superscript marker)
|
||
var commentRefs = body.Descendants<CommentReference>()
|
||
.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<HeaderPart>();
|
||
|
||
// 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<Drawing>())
|
||
{
|
||
// Look for inline or anchor images
|
||
var inline = drawing.Descendants<DW.Inline>().FirstOrDefault();
|
||
var anchor = drawing.Descendants<DW.Anchor>().FirstOrDefault();
|
||
|
||
var blipFill = (inline ?? anchor as OpenXmlElement)?
|
||
.Descendants<PIC.BlipFill>().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:
|
||
// <a:blip xmlns:a="..." r:embed="rId...">
|
||
// <a:extLst>
|
||
// <a:ext uri="http://schemas.openxmlformats.org/drawingml/2006/svg">
|
||
// <asvg:svg xmlns:asvg="..."/> <!-- SVG-specific data -->
|
||
// </a:ext>
|
||
// </a:extLst>
|
||
// </a:blip>
|
||
|
||
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<WordprocessingShapePart>() 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)
|
||
└── <w:numbering>
|
||
├── <w:abstractNum> (templates)
|
||
│ ├── <w:lvl> × 9 (levels 0-8)
|
||
│ └── <w:pPr><w:numPr> links to this abstractNum
|
||
└── <w:num> (instances)
|
||
└── <w:abstractNumId val="N"/>
|
||
```
|
||
|
||
**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<NumberingDefinitionsPart>();
|
||
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<DocumentSettingsPart>();
|
||
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
|
||
// <w:permStart w:id="1" w:editor="everyone"/>
|
||
// <w:r><w:t>Editable text</w:t></w:r>
|
||
// <w:permEnd w:id="1"/>
|
||
|
||
// 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:
|
||
// <w:permStart w:id="2" w:author="Alice"/>
|
||
|
||
// 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<DocumentSettingsPart>();
|
||
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*
|