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