Files
iofficeai--officecli/src/officecli/BlankDocCreator.cs
T
wehub-resource-sync 8cb1f9f479
Publish SDK (PyPI) / publish (push) Has been cancelled
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (windows-latest) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/sdk) (push) Has been cancelled
Publish SDK (npm) / publish (officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (macos-latest) (push) Has been cancelled
SDK smoke / smoke (ubuntu-latest) (push) Has been cancelled
Skill parity / diff (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:09:29 +08:00

716 lines
46 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 OfficeCLI (https://OfficeCLI.AI)
// SPDX-License-Identifier: Apache-2.0
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using DocumentFormat.OpenXml.Wordprocessing;
using DocumentFormat.OpenXml.Presentation;
using OfficeCli.Core;
namespace OfficeCli;
public static class BlankDocCreator
{
public static void Create(string path, string? locale = null, bool minimal = false)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
switch (ext)
{
case ".xlsx":
CreateExcel(path);
break;
case ".docx":
CreateWord(path, locale, minimal);
break;
case ".pptx":
CreatePowerPoint(path);
break;
default:
if (TryCreateViaPlugin(path, ext)) break;
throw new NotSupportedException($"Unsupported file type: {ext}. Supported: .docx, .xlsx, .pptx, or any extension served by an installed format-handler plugin that implements `create`.");
}
}
/// <summary>
/// Delegate creation of an unknown extension to a registered format-handler
/// plugin, if one exists for that extension and exposes a `create &lt;path&gt;`
/// CLI subcommand. Returns <c>true</c> if a plugin was found and produced
/// the file successfully. Generic per plugins/plugin-protocol.md — keeps
/// BlankDocCreator format-agnostic; any plugin that implements the
/// `create` subcommand on its executable participates.
/// </summary>
private static bool TryCreateViaPlugin(string path, string ext)
{
var plugin = OfficeCli.Core.Plugins.PluginRegistry.FindFor(
OfficeCli.Core.Plugins.PluginKind.FormatHandler, ext);
if (plugin is null) return false;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = plugin.ExecutablePath,
ArgumentList = { "create", System.IO.Path.GetFullPath(path) },
UseShellExecute = false,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var proc = System.Diagnostics.Process.Start(psi);
if (proc is null) return false;
// Bound the wait: a hung plugin previously blocked `create` forever
// (stderr is the only redirected stream, so the read itself cannot
// deadlock, but WaitForExit had no timeout or Kill).
var stderrTask = proc.StandardError.ReadToEndAsync();
if (!proc.WaitForExit(60_000))
{
try { proc.Kill(true); } catch { }
throw new OfficeCli.Core.CliException(
$"Format-handler plugin '{plugin.Manifest.Name}' timed out creating {path} (60s).")
{ Code = "plugin_create_failed" };
}
var stderr = stderrTask.Result;
if (proc.ExitCode != 0)
{
// Treat unknown-subcommand exit-64 as "plugin doesn't implement
// create" — fall back to NotSupportedException so the user sees
// the same error they'd see without any plugin installed.
if (proc.ExitCode == 64) return false;
throw new OfficeCli.Core.CliException(
$"Format-handler plugin '{plugin.Manifest.Name}' failed to create {path}: {stderr.Trim()}")
{ Code = "plugin_create_failed" };
}
return true;
}
private static void CreateExcel(string path)
{
using var doc = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);
var workbookPart = doc.AddWorkbookPart();
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>("rId1");
worksheetPart.Worksheet = new Worksheet(new SheetData());
worksheetPart.Worksheet.Save();
workbookPart.Workbook = new Workbook(
new Sheets(
new Sheet { Id = "rId1", SheetId = 1, Name = "Sheet1" }
)
);
workbookPart.Workbook.Save();
// theme1.xml — every real Excel workbook ships a theme part, and so do
// officecli's docx and pptx blanks. xlsx alone omitted it, which made
// workbook `theme.*` edits silently no-op (ThemeHandler had no part to
// write into) and left theme-colour lookups empty — a cross-format
// parity gap. Stamp the same shared default theme here so theme.* set/get
// works on a freshly created workbook like it does for Word/PowerPoint.
var themePart = workbookPart.AddNewPart<DocumentFormat.OpenXml.Packaging.ThemePart>();
themePart.Theme = BuildDefaultTheme(null, null);
themePart.Theme.Save();
OfficeCliMetadata.StampOnCreate(doc);
}
private static void CreateWord(string path, string? locale = null, bool minimal = false)
{
using var doc = WordprocessingDocument.Create(path, WordprocessingDocumentType.Document);
var mainPart = doc.AddMainDocumentPart();
// Locale-implied RTL — Arabic, Hebrew, Persian, Urdu, … get bidi
// defaults stamped onto sectPr + pPrDefault so users don't have to
// set direction=rtl on every paragraph.
bool isRtl = OfficeCli.Core.LocaleFontRegistry.IsRightToLeft(locale);
// Section with A4 page size, standard margins, and no docGrid snap.
// <w:bidi/> on sectPr makes the section's layout RTL (column order,
// anchor edge for page numbers, etc.) when the locale is RTL.
// CT_SectPr schema order: pgSz → pgMar → … → bidi → docGrid.
var sectPr = new SectionProperties(
new PageSize { Width = WordPageDefaults.A4WidthTwips, Height = WordPageDefaults.A4HeightTwips },
new PageMargin { Top = 1440, Right = 1800U, Bottom = 1440, Left = 1800U }
);
if (isRtl)
sectPr.AppendChild(new BiDi());
sectPr.AppendChild(new DocGrid { Type = DocGridValues.Default });
// Compatibility: do not compress punctuation spacing
// Schema order: characterSpacingControl must come before compat in w:settings
//
// `compatibilityMode=15` is the Word 2013+ modern-mode flag.
// Without it, Word opens the doc in "Compatibility Mode" (the title
// bar shows the indicator and the UI silently disables several
// newer features). Word's own blank-document save always stamps
// this value; we did not, so every doc officecli generated looked
// like a Word 2010 file to readers. The other four compatSettings
// listed below are what current Word writes alongside; including
// them keeps the settings block byte-similar to Word's own output
// so subsequent edits by Word don't churn this block.
var settings = new DocumentFormat.OpenXml.Wordprocessing.Settings(
new CharacterSpacingControl { Val = CharacterSpacingValues.DoNotCompress },
new Compatibility(
new SpaceForUnderline(),
new BalanceSingleByteDoubleByteWidth(),
new DoNotLeaveBackslashAlone(),
new UnderlineTrailingSpaces(),
new DoNotExpandShiftReturn(),
new AdjustLineHeightInTable(),
new CompatibilitySetting
{
Name = new EnumValue<CompatSettingNameValues>(CompatSettingNameValues.CompatibilityMode),
Val = new StringValue("15"),
Uri = new StringValue("http://schemas.microsoft.com/office/word")
},
new CompatibilitySetting
{
Name = new EnumValue<CompatSettingNameValues>(CompatSettingNameValues.OverrideTableStyleFontSizeAndJustification),
Val = new StringValue("1"),
Uri = new StringValue("http://schemas.microsoft.com/office/word")
},
new CompatibilitySetting
{
Name = new EnumValue<CompatSettingNameValues>(CompatSettingNameValues.EnableOpenTypeFeatures),
Val = new StringValue("1"),
Uri = new StringValue("http://schemas.microsoft.com/office/word")
},
new CompatibilitySetting
{
Name = new EnumValue<CompatSettingNameValues>(CompatSettingNameValues.DoNotFlipMirrorIndents),
Val = new StringValue("1"),
Uri = new StringValue("http://schemas.microsoft.com/office/word")
},
new CompatibilitySetting
{
Name = new EnumValue<CompatSettingNameValues>(CompatSettingNameValues.DifferentiateMultirowTableHeaders),
Val = new StringValue("1"),
Uri = new StringValue("http://schemas.microsoft.com/office/word")
}
)
);
// i18n: stamp themeFontLang from --locale so HTML preview, screen
// readers, and Word's per-script font fallback know
// the document's primary language. Routes the locale to EastAsia
// (CJK), Bidi (Arabic / Hebrew / Persian / Urdu / Thai / Hindi),
// or the bare Val attribute otherwise.
if (!string.IsNullOrEmpty(locale))
{
var tfl = new DocumentFormat.OpenXml.Wordprocessing.ThemeFontLanguages();
var langKey = locale.Replace('_', '-').ToLowerInvariant().Split('-')[0];
switch (langKey)
{
case "zh":
case "ja":
case "ko":
tfl.EastAsia = locale;
break;
case "ar":
case "he":
case "fa":
case "ur":
case "th":
case "hi":
tfl.Bidi = locale;
break;
default:
tfl.Val = locale;
break;
}
// CT_Settings sequence: characterSpacingControl (~pos 63),
// compat (~pos 78), themeFontLang (~pos 80). themeFontLang
// belongs AFTER compat, not at the front. Earlier revisions of
// this file put it before characterSpacingControl which made
// every RTL-locale doc fail OOXML validation with "unexpected
// child characterSpacingControl" — the validator was actually
// complaining about themeFontLang being out of order, but
// surfaced the next sibling as the offending element.
var compat = settings.GetFirstChild<Compatibility>();
if (compat != null) compat.InsertAfterSelf(tfl);
else settings.AppendChild(tfl);
}
var settingsPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.DocumentSettingsPart>();
settingsPart.Settings = settings;
settingsPart.Settings.Save();
var document = new Document(new Body(sectPr));
// Declare common namespaces on <w:document> so later raw-set
// injections (DrawingML textboxes <wps:wsp>, VML fallbacks <v:shape>,
// pictures <pic:pic>, math <m:oMath>, ...) validate without each
// call site re-declaring them. Mirrors what Word itself stamps on
// save. Without this, mc:AlternateContent / mc:Choice Requires="wps"
// fails MarkupCompatibility validation because the wps prefix is
// not in scope at the AlternateContent element.
document.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
document.AddNamespaceDeclaration("m", "http://schemas.openxmlformats.org/officeDocument/2006/math");
document.AddNamespaceDeclaration("v", "urn:schemas-microsoft-com:vml");
document.AddNamespaceDeclaration("o", "urn:schemas-microsoft-com:office:office");
document.AddNamespaceDeclaration("w10", "urn:schemas-microsoft-com:office:word");
document.AddNamespaceDeclaration("wne", "http://schemas.microsoft.com/office/word/2006/wordml");
document.AddNamespaceDeclaration("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing");
document.AddNamespaceDeclaration("wp14", "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing");
// w14 (Office 2010 wordml extensions — paraId / textId / docId) was
// listed in mc:Ignorable below but never declared on the root, so
// every blank docx failed validation with "Ignorable contains an
// invalid prefix 'w14'". paraId attributes do get emitted by Add
// helpers on every paragraph, so leaving this undeclared was a
// real schema violation, not cosmetic.
document.AddNamespaceDeclaration("w14", "http://schemas.microsoft.com/office/word/2010/wordml");
document.AddNamespaceDeclaration("a", "http://schemas.openxmlformats.org/drawingml/2006/main");
document.AddNamespaceDeclaration("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture");
document.AddNamespaceDeclaration("wps", "http://schemas.microsoft.com/office/word/2010/wordprocessingShape");
document.AddNamespaceDeclaration("wpg", "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup");
document.AddNamespaceDeclaration("wpi", "http://schemas.microsoft.com/office/word/2010/wordprocessingInk");
document.AddNamespaceDeclaration("wpc", "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas");
document.AddNamespaceDeclaration("w15", "http://schemas.microsoft.com/office/word/2012/wordml");
// Mark 2010+/2012 namespaces as Ignorable so older readers degrade gracefully.
document.MCAttributes ??= new DocumentFormat.OpenXml.MarkupCompatibilityAttributes();
var existingIgnorable = document.MCAttributes.Ignorable?.Value;
var ignorableTokens = new System.Collections.Generic.HashSet<string>(
(existingIgnorable ?? "").Split(' ', System.StringSplitOptions.RemoveEmptyEntries));
// Only mark prefixes that appear unwrapped (outside mc:AlternateContent)
// as Ignorable — w14/wp14/w15 carry attributes like paraId/anchorId
// directly. wps/wpg/wpi/wpc only appear inside mc:Choice and are
// already gated by mc:Fallback, so they don't need (and shouldn't get)
// Ignorable. Mirrors the docxexport MainXmlNamespaces.
foreach (var p in new[] { "w14", "w15", "wp14" })
ignorableTokens.Add(p);
document.MCAttributes.Ignorable = string.Join(" ", ignorableTokens);
mainPart.Document = document;
// Two paths: full (default) emits Word-aligned baseline (Calibri 11pt
// + Normal style + theme1.xml — matches the de-facto baseline, which
// is what Word actually writes); minimal emits raw OOXML (TNR, no sz,
// no Normal, no theme). The
// minimal path is the prior officecli behavior; the full path was
// added so docs created by officecli render identically in Word /
// / cli preview without relying on each renderer's
// Normal.dotm fallback heuristics.
//
// Resolve locale-specific defaults from LocaleFontRegistry.
// Without a locale, only Latin slots are populated so the
// host application's UI-locale defaults fill EastAsia / CS as needed.
var (locLatin, locEa, locCs) = OfficeCli.Core.LocaleFontRegistry.Resolve(locale);
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
if (minimal)
{
// Minimal path: docDefaults with rFonts only (Times New Roman),
// no sz, no spacing, no Normal style, no theme. Use this for
// testing the cli reader's fallback path or producing maximally
// compact output. Matches `officecli create` output before the
// Word-aligned baseline was added.
var minDocDefaultFonts = new RunFonts
{
Ascii = locLatin ?? "Times New Roman",
HighAnsi = locLatin ?? "Times New Roman",
};
if (!string.IsNullOrEmpty(locEa)) minDocDefaultFonts.EastAsia = locEa;
if (!string.IsNullOrEmpty(locCs)) minDocDefaultFonts.ComplexScript = locCs;
// pPrDefault/bidi for RTL locales — sets paragraph direction as
// doc-wide default so any paragraph added later inherits RTL
// without per-paragraph direction=rtl. Schema-correct location
// for the default — rPrDefault/rtl is rejected by the OOXML
// validator (CT_RPrDefault excludes <w:rtl/>); pPrDefault/bidi
// is the canonical path Word uses.
var pPrDefaultBase = isRtl ? new ParagraphPropertiesBaseStyle(new BiDi()) : null;
stylesPart.Styles = new Styles(
new DocDefaults(
new RunPropertiesDefault(new RunPropertiesBaseStyle(minDocDefaultFonts)),
pPrDefaultBase != null
? new ParagraphPropertiesDefault(pPrDefaultBase)
: new ParagraphPropertiesDefault()
)
);
stylesPart.Styles.Save();
}
else
{
var docDefaultFonts = new RunFonts
{
Ascii = locLatin ?? OfficeDefaultFonts.MinorLatin, // Calibri
HighAnsi = locLatin ?? OfficeDefaultFonts.MinorLatin,
};
if (!string.IsNullOrEmpty(locEa)) docDefaultFonts.EastAsia = locEa;
if (!string.IsNullOrEmpty(locCs)) docDefaultFonts.ComplexScript = locCs;
// Normal style — default="1". Carry the Office 2013+ Normal
// baseline (line=259/1.08 ×, no after) on the Normal pPr itself,
// not on pPrDefault — cli's reader only walks the style chain via
// ResolveSpacingFromStyle and doesn't yet inherit from pPrDefault.
// Putting it on Normal keeps pPrDefault free for paragraph-shape
// defaults (autoSpaceDE/DN, kinsoku, …) without spacing leakage.
//
// Why 1.08 × not 1.15 ×: empirical (stress-C measurement) — when
// a list line has a 14 pt marker over 11 pt body, Word renders
// the line at 14 × 1.08 × calibri-ratio = 18.45pt; cli with
// 1.15 × renders at 14 × 1.15 × ratio = 19.65pt (1.3pt/paragraph drift
// accumulating across the doc). Office 2013+ Normal IS 1.08 ×;
// matching that here matches what Word actually does.
var normalStyle = new Style(
new StyleName { Val = "Normal" },
new PrimaryStyle(),
new StyleParagraphProperties(
new SpacingBetweenLines
{
After = "0",
Line = "259",
LineRule = LineSpacingRuleValues.Auto,
}
)
)
{
Type = StyleValues.Paragraph,
StyleId = "Normal",
Default = true,
};
// pPrDefault/bidi for RTL locales — see minimal-path comment above.
var pPrDefaultBaseN = isRtl ? new ParagraphPropertiesBaseStyle(new BiDi()) : null;
stylesPart.Styles = new Styles(
new DocDefaults(
new RunPropertiesDefault(
new RunPropertiesBaseStyle(
docDefaultFonts,
new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" }, // 11pt
new FontSizeComplexScript { Val = "22" }
)
),
pPrDefaultBaseN != null
? new ParagraphPropertiesDefault(pPrDefaultBaseN)
: new ParagraphPropertiesDefault()
),
normalStyle
);
stylesPart.Styles.Save();
}
// Declare + Ignore the w14 (Office 2010 wordml) extension namespace on
// the styles root. A dumped docDefaults can carry <w14:ligatures> (and
// similar w14 run properties) raw-set verbatim from the source; without
// mc:Ignorable covering w14 the strict validator rejects them ("invalid
// child element w14:ligatures") even though Word itself tolerates them.
// Mirrors the document-root mc:Ignorable handling above.
stylesPart.Styles.AddNamespaceDeclaration("w14", "http://schemas.microsoft.com/office/word/2010/wordml");
stylesPart.Styles.MCAttributes ??= new DocumentFormat.OpenXml.MarkupCompatibilityAttributes();
var stylesIgnorable = new HashSet<string>(
(stylesPart.Styles.MCAttributes.Ignorable?.Value ?? "")
.Split(' ', System.StringSplitOptions.RemoveEmptyEntries));
if (stylesIgnorable.Add("w14"))
stylesPart.Styles.MCAttributes.Ignorable = string.Join(" ", stylesIgnorable);
stylesPart.Styles.Save();
// theme1.xml — Office's minor=Calibri / major=Calibri Light. Without
// a theme part, anything that looks up `themeFonts` (heading/body
// theme references in styles.xml) gets nothing — emit a minimal
// theme so future styles can reference it. Skipped on the minimal
// path so its output stays free of theme dependencies.
if (!minimal)
{
var themePart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.ThemePart>();
themePart.Theme = BuildDefaultTheme(locEa, locCs);
themePart.Theme.Save();
}
var numberingPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.NumberingDefinitionsPart>();
numberingPart.Numbering = new DocumentFormat.OpenXml.Wordprocessing.Numbering();
numberingPart.Numbering.Save();
mainPart.Document.Save();
OfficeCliMetadata.StampOnCreate(doc);
}
/// <summary>
/// Builds the standard Office Word theme (clrScheme + fontScheme + fmtScheme)
/// that a blank document stamps into theme1.xml. Shared with
/// <c>WordBatchEmitter.EmitThemeRaw</c> so a dump of a theme-less source
/// emits a schema-complete theme placeholder (one Word can open) rather
/// than a bare &lt;a:theme/&gt; stub. Pass the locale-resolved East-Asian /
/// complex-script typefaces (or null for the locale-neutral default).
/// </summary>
internal static DocumentFormat.OpenXml.Drawing.Theme BuildDefaultTheme(string? locEa, string? locCs)
=> new DocumentFormat.OpenXml.Drawing.Theme(
new DocumentFormat.OpenXml.Drawing.ThemeElements(
new DocumentFormat.OpenXml.Drawing.ColorScheme(
new DocumentFormat.OpenXml.Drawing.Dark1Color(new DocumentFormat.OpenXml.Drawing.SystemColor { Val = DocumentFormat.OpenXml.Drawing.SystemColorValues.WindowText, LastColor = "000000" }),
new DocumentFormat.OpenXml.Drawing.Light1Color(new DocumentFormat.OpenXml.Drawing.SystemColor { Val = DocumentFormat.OpenXml.Drawing.SystemColorValues.Window, LastColor = "FFFFFF" }),
new DocumentFormat.OpenXml.Drawing.Dark2Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Dark2 }),
new DocumentFormat.OpenXml.Drawing.Light2Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Light2 }),
new DocumentFormat.OpenXml.Drawing.Accent1Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent1 }),
new DocumentFormat.OpenXml.Drawing.Accent2Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent2 }),
new DocumentFormat.OpenXml.Drawing.Accent3Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent3 }),
new DocumentFormat.OpenXml.Drawing.Accent4Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent4 }),
new DocumentFormat.OpenXml.Drawing.Accent5Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent5 }),
new DocumentFormat.OpenXml.Drawing.Accent6Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent6 }),
new DocumentFormat.OpenXml.Drawing.Hyperlink(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Hyperlink }),
new DocumentFormat.OpenXml.Drawing.FollowedHyperlinkColor(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.FollowedHyperlink })
) { Name = "Office" },
new DocumentFormat.OpenXml.Drawing.FontScheme(
new DocumentFormat.OpenXml.Drawing.MajorFont(
new DocumentFormat.OpenXml.Drawing.LatinFont { Typeface = OfficeDefaultFonts.MajorLatin },
new DocumentFormat.OpenXml.Drawing.EastAsianFont { Typeface = locEa ?? "" },
new DocumentFormat.OpenXml.Drawing.ComplexScriptFont { Typeface = locCs ?? "" }
),
new DocumentFormat.OpenXml.Drawing.MinorFont(
new DocumentFormat.OpenXml.Drawing.LatinFont { Typeface = OfficeDefaultFonts.MinorLatin },
new DocumentFormat.OpenXml.Drawing.EastAsianFont { Typeface = locEa ?? "" },
new DocumentFormat.OpenXml.Drawing.ComplexScriptFont { Typeface = locCs ?? "" }
)
) { Name = "Office" },
new DocumentFormat.OpenXml.Drawing.FormatScheme(
new DocumentFormat.OpenXml.Drawing.FillStyleList(
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })
),
new DocumentFormat.OpenXml.Drawing.LineStyleList(
new DocumentFormat.OpenXml.Drawing.Outline(new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })) { Width = 6350, CapType = DocumentFormat.OpenXml.Drawing.LineCapValues.Flat },
new DocumentFormat.OpenXml.Drawing.Outline(new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })) { Width = 12700, CapType = DocumentFormat.OpenXml.Drawing.LineCapValues.Flat },
new DocumentFormat.OpenXml.Drawing.Outline(new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })) { Width = 19050, CapType = DocumentFormat.OpenXml.Drawing.LineCapValues.Flat }
),
new DocumentFormat.OpenXml.Drawing.EffectStyleList(
new DocumentFormat.OpenXml.Drawing.EffectStyle(new DocumentFormat.OpenXml.Drawing.EffectList()),
new DocumentFormat.OpenXml.Drawing.EffectStyle(new DocumentFormat.OpenXml.Drawing.EffectList()),
new DocumentFormat.OpenXml.Drawing.EffectStyle(new DocumentFormat.OpenXml.Drawing.EffectList())
),
new DocumentFormat.OpenXml.Drawing.BackgroundFillStyleList(
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })
)
) { Name = "Office" }
)
) { Name = "Office Theme" };
private static void CreatePowerPoint(string path)
{
using var doc = PresentationDocument.Create(path, PresentationDocumentType.Presentation);
var presentationPart = doc.AddPresentationPart();
// Create SlideMaster + SlideLayout (required by spec)
var slideMasterPart = presentationPart.AddNewPart<DocumentFormat.OpenXml.Packaging.SlideMasterPart>("rId1");
var slideLayoutPart = slideMasterPart.AddNewPart<DocumentFormat.OpenXml.Packaging.SlideLayoutPart>("rId1");
// Theme must be under presentationPart, then shared to slideMaster
var themePart = presentationPart.AddNewPart<DocumentFormat.OpenXml.Packaging.ThemePart>("rId2");
slideMasterPart.AddPart(themePart);
themePart.Theme = new DocumentFormat.OpenXml.Drawing.Theme(
new DocumentFormat.OpenXml.Drawing.ThemeElements(
new DocumentFormat.OpenXml.Drawing.ColorScheme(
new DocumentFormat.OpenXml.Drawing.Dark1Color(new DocumentFormat.OpenXml.Drawing.SystemColor { Val = DocumentFormat.OpenXml.Drawing.SystemColorValues.WindowText, LastColor = "000000" }),
new DocumentFormat.OpenXml.Drawing.Light1Color(new DocumentFormat.OpenXml.Drawing.SystemColor { Val = DocumentFormat.OpenXml.Drawing.SystemColorValues.Window, LastColor = "FFFFFF" }),
new DocumentFormat.OpenXml.Drawing.Dark2Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Dark2 }),
new DocumentFormat.OpenXml.Drawing.Light2Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Light2 }),
new DocumentFormat.OpenXml.Drawing.Accent1Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent1 }),
new DocumentFormat.OpenXml.Drawing.Accent2Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent2 }),
new DocumentFormat.OpenXml.Drawing.Accent3Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent3 }),
new DocumentFormat.OpenXml.Drawing.Accent4Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent4 }),
new DocumentFormat.OpenXml.Drawing.Accent5Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent5 }),
new DocumentFormat.OpenXml.Drawing.Accent6Color(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Accent6 }),
new DocumentFormat.OpenXml.Drawing.Hyperlink(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.Hyperlink }),
new DocumentFormat.OpenXml.Drawing.FollowedHyperlinkColor(new DocumentFormat.OpenXml.Drawing.RgbColorModelHex { Val = OfficeDefaultThemeColors.FollowedHyperlink })
) { Name = "Office" },
new DocumentFormat.OpenXml.Drawing.FontScheme(
new DocumentFormat.OpenXml.Drawing.MajorFont(
new DocumentFormat.OpenXml.Drawing.LatinFont { Typeface = OfficeDefaultFonts.MajorLatin },
new DocumentFormat.OpenXml.Drawing.EastAsianFont { Typeface = "" },
new DocumentFormat.OpenXml.Drawing.ComplexScriptFont { Typeface = "" }
),
new DocumentFormat.OpenXml.Drawing.MinorFont(
new DocumentFormat.OpenXml.Drawing.LatinFont { Typeface = OfficeDefaultFonts.MinorLatin },
new DocumentFormat.OpenXml.Drawing.EastAsianFont { Typeface = "" },
new DocumentFormat.OpenXml.Drawing.ComplexScriptFont { Typeface = "" }
)
) { Name = "Office" },
new DocumentFormat.OpenXml.Drawing.FormatScheme(
new DocumentFormat.OpenXml.Drawing.FillStyleList(
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })
),
new DocumentFormat.OpenXml.Drawing.LineStyleList(
new DocumentFormat.OpenXml.Drawing.Outline(new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })) { Width = 6350, CapType = DocumentFormat.OpenXml.Drawing.LineCapValues.Flat },
new DocumentFormat.OpenXml.Drawing.Outline(new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })) { Width = 12700, CapType = DocumentFormat.OpenXml.Drawing.LineCapValues.Flat },
new DocumentFormat.OpenXml.Drawing.Outline(new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })) { Width = 19050, CapType = DocumentFormat.OpenXml.Drawing.LineCapValues.Flat }
),
new DocumentFormat.OpenXml.Drawing.EffectStyleList(
new DocumentFormat.OpenXml.Drawing.EffectStyle(new DocumentFormat.OpenXml.Drawing.EffectList()),
new DocumentFormat.OpenXml.Drawing.EffectStyle(new DocumentFormat.OpenXml.Drawing.EffectList()),
new DocumentFormat.OpenXml.Drawing.EffectStyle(new DocumentFormat.OpenXml.Drawing.EffectList())
),
new DocumentFormat.OpenXml.Drawing.BackgroundFillStyleList(
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor }),
new DocumentFormat.OpenXml.Drawing.SolidFill(new DocumentFormat.OpenXml.Drawing.SchemeColor { Val = DocumentFormat.OpenXml.Drawing.SchemeColorValues.PhColor })
)
) { Name = "Office" }
)
) { Name = "Office Theme" };
themePart.Theme.Save();
// Layout 1: Blank
slideLayoutPart.SlideLayout = new DocumentFormat.OpenXml.Presentation.SlideLayout(
new DocumentFormat.OpenXml.Presentation.CommonSlideData(
new DocumentFormat.OpenXml.Presentation.ShapeTree(
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(
new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1, Name = "" },
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()
),
new DocumentFormat.OpenXml.Presentation.GroupShapeProperties()
)
) { Name = "Blank" }
) { Type = DocumentFormat.OpenXml.Presentation.SlideLayoutValues.Blank };
slideLayoutPart.SlideLayout.Save();
slideLayoutPart.AddPart(slideMasterPart);
// Layout 2: Title Slide (title + subtitle)
var titleLayoutPart = slideMasterPart.AddNewPart<DocumentFormat.OpenXml.Packaging.SlideLayoutPart>("rId2");
titleLayoutPart.SlideLayout = new DocumentFormat.OpenXml.Presentation.SlideLayout(
new DocumentFormat.OpenXml.Presentation.CommonSlideData(
new DocumentFormat.OpenXml.Presentation.ShapeTree(
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(
new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1, Name = "" },
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()
),
new DocumentFormat.OpenXml.Presentation.GroupShapeProperties(),
CreateLayoutPlaceholder(2, "Title", PlaceholderValues.CenteredTitle, 685800, 2130425, 7772400, 1470025),
CreateLayoutPlaceholder(3, "Subtitle", PlaceholderValues.SubTitle, 1371600, 3886200, 6400800, 1752600, idx: 1)
)
) { Name = "Title Slide" }
) { Type = DocumentFormat.OpenXml.Presentation.SlideLayoutValues.Title };
titleLayoutPart.SlideLayout.Save();
titleLayoutPart.AddPart(slideMasterPart);
// Layout 3: Title and Content
var contentLayoutPart = slideMasterPart.AddNewPart<DocumentFormat.OpenXml.Packaging.SlideLayoutPart>("rId3");
contentLayoutPart.SlideLayout = new DocumentFormat.OpenXml.Presentation.SlideLayout(
new DocumentFormat.OpenXml.Presentation.CommonSlideData(
new DocumentFormat.OpenXml.Presentation.ShapeTree(
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(
new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1, Name = "" },
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()
),
new DocumentFormat.OpenXml.Presentation.GroupShapeProperties(),
CreateLayoutPlaceholder(2, "Title", PlaceholderValues.Title, 838200, 365125, 10515600, 1325563),
CreateLayoutPlaceholder(3, "Content", PlaceholderValues.Body, 838200, 1825625, 10515600, 4351338, idx: 1)
)
) { Name = "Title and Content" }
) { Type = DocumentFormat.OpenXml.Presentation.SlideLayoutValues.ObjectText };
contentLayoutPart.SlideLayout.Save();
contentLayoutPart.AddPart(slideMasterPart);
// Layout 4: Two Content
var twoContentLayoutPart = slideMasterPart.AddNewPart<DocumentFormat.OpenXml.Packaging.SlideLayoutPart>("rId4");
twoContentLayoutPart.SlideLayout = new DocumentFormat.OpenXml.Presentation.SlideLayout(
new DocumentFormat.OpenXml.Presentation.CommonSlideData(
new DocumentFormat.OpenXml.Presentation.ShapeTree(
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(
new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1, Name = "" },
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()
),
new DocumentFormat.OpenXml.Presentation.GroupShapeProperties(),
CreateLayoutPlaceholder(2, "Title", PlaceholderValues.Title, 838200, 365125, 10515600, 1325563),
CreateLayoutPlaceholder(3, "Content Left", PlaceholderValues.Body, 838200, 1825625, 5181600, 4351338, idx: 1),
CreateLayoutPlaceholder(4, "Content Right", PlaceholderValues.Body, 6172200, 1825625, 5181600, 4351338, idx: 2)
)
) { Name = "Two Content" }
) { Type = DocumentFormat.OpenXml.Presentation.SlideLayoutValues.TwoColumnText };
twoContentLayoutPart.SlideLayout.Save();
twoContentLayoutPart.AddPart(slideMasterPart);
// Layout 5: Title Only (title placeholder, no body)
var titleOnlyLayoutPart = slideMasterPart.AddNewPart<DocumentFormat.OpenXml.Packaging.SlideLayoutPart>("rId5");
titleOnlyLayoutPart.SlideLayout = new DocumentFormat.OpenXml.Presentation.SlideLayout(
new DocumentFormat.OpenXml.Presentation.CommonSlideData(
new DocumentFormat.OpenXml.Presentation.ShapeTree(
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(
new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1, Name = "" },
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()
),
new DocumentFormat.OpenXml.Presentation.GroupShapeProperties(),
CreateLayoutPlaceholder(2, "Title", PlaceholderValues.Title, 838200, 365125, 10515600, 1325563)
)
) { Name = "Title Only" }
) { Type = DocumentFormat.OpenXml.Presentation.SlideLayoutValues.TitleOnly };
titleOnlyLayoutPart.SlideLayout.Save();
titleOnlyLayoutPart.AddPart(slideMasterPart);
slideMasterPart.SlideMaster = new DocumentFormat.OpenXml.Presentation.SlideMaster(
new DocumentFormat.OpenXml.Presentation.CommonSlideData(
new DocumentFormat.OpenXml.Presentation.ShapeTree(
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(
new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1, Name = "" },
new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()
),
new DocumentFormat.OpenXml.Presentation.GroupShapeProperties()
)
),
new DocumentFormat.OpenXml.Presentation.ColorMap
{
Background1 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Light1,
Text1 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Dark1,
Background2 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Light2,
Text2 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Dark2,
Accent1 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Accent1,
Accent2 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Accent2,
Accent3 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Accent3,
Accent4 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Accent4,
Accent5 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Accent5,
Accent6 = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Accent6,
Hyperlink = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.Hyperlink,
FollowedHyperlink = DocumentFormat.OpenXml.Drawing.ColorSchemeIndexValues.FollowedHyperlink,
},
new DocumentFormat.OpenXml.Presentation.SlideLayoutIdList(
new DocumentFormat.OpenXml.Presentation.SlideLayoutId { Id = 2147483649, RelationshipId = "rId1" },
new DocumentFormat.OpenXml.Presentation.SlideLayoutId { Id = 2147483650, RelationshipId = "rId2" },
new DocumentFormat.OpenXml.Presentation.SlideLayoutId { Id = 2147483651, RelationshipId = "rId3" },
new DocumentFormat.OpenXml.Presentation.SlideLayoutId { Id = 2147483652, RelationshipId = "rId4" },
new DocumentFormat.OpenXml.Presentation.SlideLayoutId { Id = 2147483653, RelationshipId = "rId5" }
)
);
slideMasterPart.SlideMaster.Save();
presentationPart.Presentation = new DocumentFormat.OpenXml.Presentation.Presentation(
new DocumentFormat.OpenXml.Presentation.SlideMasterIdList(
new DocumentFormat.OpenXml.Presentation.SlideMasterId { Id = 2147483648, RelationshipId = "rId1" }
),
new SlideIdList(),
new SlideSize { Cx = (int)SlideSizeDefaults.Widescreen16x9Cx, Cy = (int)SlideSizeDefaults.Widescreen16x9Cy },
new NotesSize { Cx = SlideSizeDefaults.NotesPortraitCx, Cy = SlideSizeDefaults.NotesPortraitCy }
);
presentationPart.Presentation.Save();
OfficeCliMetadata.StampOnCreate(doc);
}
private static Shape CreateLayoutPlaceholder(uint id, string name, PlaceholderValues phType,
long x, long y, long cx, long cy, uint? idx = null)
{
var shape = new Shape();
// OOXML convention (PowerPoint templates): Title/CenteredTitle placeholders
// omit @idx (defaults to 0); SubTitle / Body / Footer / Date / SlideNumber
// slots carry an explicit @idx so slide-side <p:ph idx="N"/> can bind back to
// the matching layout placeholder during inheritance resolution.
var placeholder = new PlaceholderShape { Type = phType };
if (idx.HasValue) placeholder.Index = idx.Value;
shape.NonVisualShapeProperties = new NonVisualShapeProperties(
new NonVisualDrawingProperties { Id = id, Name = name },
new NonVisualShapeDrawingProperties(new DocumentFormat.OpenXml.Drawing.ShapeLocks { NoGrouping = true }),
new ApplicationNonVisualDrawingProperties(placeholder)
);
shape.ShapeProperties = new ShapeProperties(
new DocumentFormat.OpenXml.Drawing.Transform2D(
new DocumentFormat.OpenXml.Drawing.Offset { X = x, Y = y },
new DocumentFormat.OpenXml.Drawing.Extents { Cx = cx, Cy = cy }
)
);
shape.TextBody = new TextBody(
new DocumentFormat.OpenXml.Drawing.BodyProperties(),
new DocumentFormat.OpenXml.Drawing.ListStyle(),
new DocumentFormat.OpenXml.Drawing.Paragraph(
new DocumentFormat.OpenXml.Drawing.EndParagraphRunProperties { Language = "en-US" })
);
return shape;
}
}