// 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`."); } } /// /// Delegate creation of an unknown extension to a registered format-handler /// plugin, if one exists for that extension and exposes a `create <path>` /// CLI subcommand. Returns true 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. /// 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("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(); 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. // 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.CompatibilityMode), Val = new StringValue("15"), Uri = new StringValue("http://schemas.microsoft.com/office/word") }, new CompatibilitySetting { Name = new EnumValue(CompatSettingNameValues.OverrideTableStyleFontSizeAndJustification), Val = new StringValue("1"), Uri = new StringValue("http://schemas.microsoft.com/office/word") }, new CompatibilitySetting { Name = new EnumValue(CompatSettingNameValues.EnableOpenTypeFeatures), Val = new StringValue("1"), Uri = new StringValue("http://schemas.microsoft.com/office/word") }, new CompatibilitySetting { Name = new EnumValue(CompatSettingNameValues.DoNotFlipMirrorIndents), Val = new StringValue("1"), Uri = new StringValue("http://schemas.microsoft.com/office/word") }, new CompatibilitySetting { Name = new EnumValue(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(); if (compat != null) compat.InsertAfterSelf(tfl); else settings.AppendChild(tfl); } var settingsPart = mainPart.AddNewPart(); settingsPart.Settings = settings; settingsPart.Settings.Save(); var document = new Document(new Body(sectPr)); // Declare common namespaces on so later raw-set // injections (DrawingML textboxes , VML fallbacks , // pictures , math , ...) 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( (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(); 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 ); 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 (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( (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(); themePart.Theme = BuildDefaultTheme(locEa, locCs); themePart.Theme.Save(); } var numberingPart = mainPart.AddNewPart(); numberingPart.Numbering = new DocumentFormat.OpenXml.Wordprocessing.Numbering(); numberingPart.Numbering.Save(); mainPart.Document.Save(); OfficeCliMetadata.StampOnCreate(doc); } /// /// Builds the standard Office Word theme (clrScheme + fontScheme + fmtScheme) /// that a blank document stamps into theme1.xml. Shared with /// WordBatchEmitter.EmitThemeRaw so a dump of a theme-less source /// emits a schema-complete theme placeholder (one Word can open) rather /// than a bare <a:theme/> stub. Pass the locale-resolved East-Asian / /// complex-script typefaces (or null for the locale-neutral default). /// 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("rId1"); var slideLayoutPart = slideMasterPart.AddNewPart("rId1"); // Theme must be under presentationPart, then shared to slideMaster var themePart = presentationPart.AddNewPart("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("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("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("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("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 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; } }