From 57ce521fc941ce9fdc909557dcd909f222c6a222 Mon Sep 17 00:00:00 2001
From: forest6511 <20209757+forest6511@users.noreply.github.com>
Date: Mon, 4 May 2026 10:45:50 +0900
Subject: [PATCH] fix: also emit mirrorMargins inside sectPr for Word for Mac
PDF export (#74)
Word for Mac's "Save as PDF" / "Print to PDF" pipelines flatten the mirror on
verso pages when lives only in settings.xml (the
spec-compliant CT_Settings location). Defensively duplicate the element
inside sectPr so the export pipeline picks it up; LibreOffice and Word for
Windows ignore unknown sectPr children, so existing behaviour is preserved.
This unblocks KDP paperback workflows that rely on MirrorMargins +
MarginGutter to satisfy the 22.225 mm inner-margin requirement.
---
.../OpenXml/OpenXmlDocumentBuilder.cs | 10 ++
.../MarkdownToDocxIntegrationTests.cs | 97 +++++++++++++++++++
2 files changed, 107 insertions(+)
diff --git a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
index d617f7d..b879a0a 100644
--- a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
+++ b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
@@ -109,6 +109,16 @@ private SectionProperties CreateSectionProperties()
Gutter = (uint)pageConfig.GutterMargin
});
+ // Word for Mac's "Save as PDF" / "Print to PDF" pipeline does not honor
+ // when it is only placed in settings.xml (spec-compliant
+ // location). Duplicating it inside sectPr makes the export pipeline mirror
+ // verso pages correctly while remaining harmless for Word/Windows and
+ // LibreOffice, which ignore unknown sectPr children. See issue #74.
+ if (pageConfig.MirrorMargins)
+ {
+ sectionProps.AppendChild(new W.MirrorMargins());
+ }
+
return sectionProps;
}
diff --git a/csharp-version/tests/MarkdownToDocx.Tests/Integration/MarkdownToDocxIntegrationTests.cs b/csharp-version/tests/MarkdownToDocx.Tests/Integration/MarkdownToDocxIntegrationTests.cs
index 1742474..7287b07 100644
--- a/csharp-version/tests/MarkdownToDocx.Tests/Integration/MarkdownToDocxIntegrationTests.cs
+++ b/csharp-version/tests/MarkdownToDocx.Tests/Integration/MarkdownToDocxIntegrationTests.cs
@@ -362,6 +362,103 @@ public void ConvertMarkdownTable_WithMirrorMargins_TableShouldUsePercentageWidth
"table width must be percentage-based to prevent margin overflow on narrow pages");
}
+ [Fact]
+ public void Build_WithMirrorMargins_ShouldEmitMirrorMarginsInBothSettingsAndSectPr()
+ {
+ // Regression test for issue #74: Word for Mac's "Save as PDF" pipeline
+ // does not honor when it is only placed in settings.xml.
+ // The element must also be duplicated inside sectPr so verso pages are
+ // mirrored correctly during PDF export.
+
+ // Arrange: PageLayout with MirrorMargins enabled (KDP paperback style).
+ var pageLayout = new PageLayoutConfig
+ {
+ Width = 14.8,
+ Height = 21.0,
+ MarginTop = 1.9,
+ MarginBottom = 1.9,
+ MarginLeft = 1.30,
+ MarginRight = 1.30,
+ MarginGutter = 1.20,
+ MirrorMargins = true
+ };
+
+ // Act
+ using var stream = new MemoryStream();
+ using (var builder = new OpenXmlDocumentBuilder(
+ stream,
+ new MarkdownToDocx.Styling.TextDirection.ConfigurableTextDirectionProvider(
+ new HorizontalTextProvider(), pageLayout)))
+ {
+ builder.Save();
+ }
+
+ // Assert: mirrorMargins present in both locations.
+ stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(stream, false);
+
+ var settings = doc.MainDocumentPart!.DocumentSettingsPart!.Settings;
+ settings.Elements().Should().HaveCount(1,
+ "spec-compliant location (CT_Settings) must keep emitting mirrorMargins");
+
+ var sectionProps = doc.MainDocumentPart.Document.Body!
+ .Elements().LastOrDefault();
+ sectionProps.Should().NotBeNull();
+ // mirrorMargins is not part of CT_SectPr in OOXML schema, so on
+ // deserialization OpenXml SDK exposes it as an OpenXmlUnknownElement
+ // (rather than the typed MirrorMargins class). Match by local name to
+ // confirm the raw XML carries it inside sectPr.
+ bool sectPrCarriesMirrorMargins = sectionProps!.ChildElements
+ .Any(e => e.LocalName == "mirrorMargins"
+ && e.NamespaceUri == "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
+ sectPrCarriesMirrorMargins.Should().BeTrue(
+ "Word for Mac PDF export requires mirrorMargins inside sectPr (issue #74)");
+ }
+
+ [Fact]
+ public void Build_WithoutMirrorMargins_ShouldNotEmitMirrorMarginsAnywhere()
+ {
+ // Arrange: PageLayout with MirrorMargins disabled.
+ var pageLayout = new PageLayoutConfig
+ {
+ Width = 14.8,
+ Height = 21.0,
+ MarginTop = 1.9,
+ MarginBottom = 1.9,
+ MarginLeft = 1.30,
+ MarginRight = 1.30,
+ MirrorMargins = false
+ };
+
+ // Act
+ using var stream = new MemoryStream();
+ using (var builder = new OpenXmlDocumentBuilder(
+ stream,
+ new MarkdownToDocx.Styling.TextDirection.ConfigurableTextDirectionProvider(
+ new HorizontalTextProvider(), pageLayout)))
+ {
+ builder.Save();
+ }
+
+ // Assert: no mirrorMargins emitted in either location.
+ stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(stream, false);
+
+ doc.MainDocumentPart!.DocumentSettingsPart.Should().BeNull(
+ "settings.xml part must not be created when MirrorMargins is false");
+
+ var sectionProps = doc.MainDocumentPart.Document.Body!
+ .Elements().LastOrDefault();
+ sectionProps.Should().NotBeNull();
+ // Match by local name (OpenXml SDK deserializes unknown sectPr children
+ // as OpenXmlUnknownElement; see Build_WithMirrorMargins for context).
+ bool sectPrCarriesMirrorMargins = sectionProps!.ChildElements
+ .Any(e => e.LocalName == "mirrorMargins"
+ && e.NamespaceUri == "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
+ sectPrCarriesMirrorMargins.Should().BeFalse(
+ "sectPr must not carry mirrorMargins when feature is disabled");
+ }
+
public void Dispose()
{
try