Fix Word-open failure: strict OOXML element ordering in sectPr and settings
sectPr children were emitted with headerReference/footerReference after pgSz..mirrorMargins (SECTPR_ORDER), and w:settings had evenAndOddHeaders / autoHyphenation appended after decimalSymbol/listSeparator. LibreOffice tolerates out-of-order XML; Word validates CT_SectPr and CT_Settings strictly and refuses the file. - Move headerReference/footerReference to the head of SECTPR_ORDER per ECMA-376 CT_SectPr. - Add SETTINGS_ORDER + _reorder_settings() using the full CT_Settings sequence (mathPr in the m: namespace) and apply it in Pass 2. - Add regression tests asserting monotonic CT_SectPr and CT_Settings order for every section. 53 tests pass.
This commit is contained in:
@@ -544,6 +544,88 @@ class RunningHeaderTests(unittest.TestCase):
|
||||
d = self._format()
|
||||
self.assertIsNotNone(d.settings.element.find(qn("w:autoHyphenation")))
|
||||
|
||||
def test_sectpr_child_order_is_ooxml_valid(self):
|
||||
# Word validates CT_SectPr child order strictly (ECMA-376); LibreOffice
|
||||
# ignores it. headerReference/footerReference must come FIRST.
|
||||
d = self._format()
|
||||
# Independent canonical sequence (subset actually produced here).
|
||||
order = [
|
||||
"headerReference", "footerReference", "footnotePr", "endnotePr",
|
||||
"type", "pgSz", "pgMar", "paperSrc", "pgBorders", "lnNumType",
|
||||
"pgNumType", "cols", "formProt", "vAlign", "noEndnote",
|
||||
"titlePg", "mirrorMargins", "textDirection", "bidi", "rtlGutter",
|
||||
"docGrid", "printerSettings", "sectPrChange",
|
||||
]
|
||||
rank = {qn("w:" + t): i for i, t in enumerate(order)}
|
||||
for sec in d.sections:
|
||||
tags = [c.tag for c in sec._sectPr]
|
||||
ranks = [rank[t] for t in tags]
|
||||
self.assertEqual(
|
||||
ranks, sorted(ranks),
|
||||
"sectPr children out of CT_SectPr order: "
|
||||
+ ", ".join(t.split("}")[-1] for t in tags))
|
||||
# References must precede every layout element.
|
||||
self.assertLess(tags.index(qn("w:headerReference")), tags.index(qn("w:pgSz")))
|
||||
self.assertLess(tags.index(qn("w:footerReference")), tags.index(qn("w:pgSz")))
|
||||
|
||||
def test_settings_child_order_is_ooxml_valid(self):
|
||||
# Word validates CT_Settings child order strictly (ECMA-376).
|
||||
d = self._format()
|
||||
order = [
|
||||
"writeProtection", "view", "zoom", "removePersonalInformation",
|
||||
"doNotDisplayPageBoundaries", "displayBackgroundShape",
|
||||
"printPostScriptOverText", "printFractionalCharacterWidth",
|
||||
"printFormsData", "embedTrueTypeFonts", "embedSystemFonts",
|
||||
"saveSubsetFonts", "saveFormsData", "mirrorMargins",
|
||||
"alignBordersAndEdges", "bordersDoNotSurroundHeader",
|
||||
"bordersDoNotSurroundFooter", "gutterAtTop", "hideSpellingErrors",
|
||||
"hideGrammaticalErrors", "activeWritingStyle", "proofState",
|
||||
"formsDesign", "attachedTemplate", "linkStyles",
|
||||
"stylePaneFormatFilter", "stylePaneSortMethod", "documentType",
|
||||
"mailMerge", "revisionView", "trackChanges", "doNotTrackMoves",
|
||||
"doNotTrackFormatting", "documentProtection", "autoFormatOverride",
|
||||
"styleLockTheme", "styleLockQFSet", "defaultTabStop",
|
||||
"autoHyphenation", "consecutiveHyphenLimit", "hyphenationZone",
|
||||
"doNotHyphenateCaps", "showEnvelope", "summaryLength",
|
||||
"clickAndTypeStyle", "defaultTableStyle", "evenAndOddHeaders",
|
||||
"bookFoldRevPrinting", "bookFoldPrinting",
|
||||
"bookFoldPrintingSheets", "drawingGridHorizontalSpacing",
|
||||
"drawingGridVerticalSpacing",
|
||||
"displayHorizontalDrawingGridEvery",
|
||||
"displayVerticalDrawingGridEvery",
|
||||
"doNotUseMarginsForDrawingGridOrigin",
|
||||
"drawingGridHorizontalOrigin", "drawingGridVerticalOrigin",
|
||||
"doNotShadeFormData", "noPunctuationKerning",
|
||||
"characterSpacingControl", "printTwoOnOne",
|
||||
"strictFirstAndLastChars", "noLineBreaksAfter",
|
||||
"noLineBreaksBefore", "savePreviewPicture",
|
||||
"doNotValidateAgainstSchema", "saveInvalidXml",
|
||||
"ignoreMixedContent", "alwaysShowPlaceholderText",
|
||||
"doNotDemarcateInvalidXml", "saveXmlDataOnly",
|
||||
"useXSLTWhenSaving", "saveThroughXslt", "showXMLTags",
|
||||
"alwaysMergeEmptyNamespace", "updateFields", "hdrShapeDefaults",
|
||||
"footnotePr", "endnotePr", "compat", "rsids", "mathPr",
|
||||
"themeFontLang", "clrSchemeMapping",
|
||||
"doNotIncludeSubdocsInStats", "doNotAutoCompressPictures",
|
||||
"forceUpgrade", "captions", "readModeInkLockDown",
|
||||
"smartTagType", "shapeDefaults", "doNotEmbedSmartTags",
|
||||
"decimalSymbol", "listSeparator",
|
||||
]
|
||||
rank = {qn("w:" + t): i for i, t in enumerate(order)}
|
||||
# w:mathPr lives in the math namespace, not w:.
|
||||
rank['{http://schemas.openxmlformats.org/officeDocument/2006/math}mathPr'] = rank.pop(qn("w:mathPr"))
|
||||
settings = d.settings.element
|
||||
tags = [c.tag for c in settings]
|
||||
# Cross-namespace extensions (e.g. w14:docId from the source doc) are
|
||||
# interleaved and tolerated by Word; only the canonical w:/m: sequence
|
||||
# must be monotonic.
|
||||
canonical = [t for t in tags if t in rank]
|
||||
ranks = [rank[t] for t in canonical]
|
||||
self.assertEqual(
|
||||
ranks, sorted(ranks),
|
||||
"w:settings children out of CT_Settings order: "
|
||||
+ ", ".join(t.split("}")[-1] for t in canonical))
|
||||
|
||||
|
||||
class WidowOrphanTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -1489,7 +1489,11 @@ def _add_section_break(doc):
|
||||
return doc.add_section()
|
||||
|
||||
|
||||
# Child order of w:sectPr per ECMA-376 CT_SectPr. Word requires this exact
|
||||
# sequence (header/footer references FIRST); LibreOffice tolerates any order.
|
||||
SECTPR_ORDER = [
|
||||
qn('w:headerReference'),
|
||||
qn('w:footerReference'),
|
||||
qn('w:footnotePr'),
|
||||
qn('w:endnotePr'),
|
||||
qn('w:type'),
|
||||
@@ -1508,8 +1512,6 @@ SECTPR_ORDER = [
|
||||
qn('w:textDirection'),
|
||||
qn('w:bidi'),
|
||||
qn('w:rtlGutter'),
|
||||
qn('w:headerReference'),
|
||||
qn('w:footerReference'),
|
||||
qn('w:docGrid'),
|
||||
qn('w:printerSettings'),
|
||||
qn('w:sectPrChange'),
|
||||
@@ -1531,6 +1533,73 @@ def _reorder_sect_pr(sect_pr):
|
||||
sect_pr.append(child)
|
||||
|
||||
|
||||
# Child order of w:settings per ECMA-376 CT_Settings. Same strictness rule:
|
||||
# Word rejects out-of-order children, LibreOffice ignores the order.
|
||||
SETTINGS_ORDER = [
|
||||
qn('w:writeProtection'), qn('w:view'), qn('w:zoom'),
|
||||
qn('w:removePersonalInformation'), qn('w:doNotDisplayPageBoundaries'),
|
||||
qn('w:displayBackgroundShape'), qn('w:printPostScriptOverText'),
|
||||
qn('w:printFractionalCharacterWidth'), qn('w:printFormsData'),
|
||||
qn('w:embedTrueTypeFonts'), qn('w:embedSystemFonts'),
|
||||
qn('w:saveSubsetFonts'), qn('w:saveFormsData'), qn('w:mirrorMargins'),
|
||||
qn('w:alignBordersAndEdges'), qn('w:bordersDoNotSurroundHeader'),
|
||||
qn('w:bordersDoNotSurroundFooter'), qn('w:gutterAtTop'),
|
||||
qn('w:hideSpellingErrors'), qn('w:hideGrammaticalErrors'),
|
||||
qn('w:activeWritingStyle'), qn('w:proofState'), qn('w:formsDesign'),
|
||||
qn('w:attachedTemplate'), qn('w:linkStyles'),
|
||||
qn('w:stylePaneFormatFilter'), qn('w:stylePaneSortMethod'),
|
||||
qn('w:documentType'), qn('w:mailMerge'), qn('w:revisionView'),
|
||||
qn('w:trackChanges'), qn('w:doNotTrackMoves'),
|
||||
qn('w:doNotTrackFormatting'), qn('w:documentProtection'),
|
||||
qn('w:autoFormatOverride'), qn('w:styleLockTheme'),
|
||||
qn('w:styleLockQFSet'), qn('w:defaultTabStop'), qn('w:autoHyphenation'),
|
||||
qn('w:consecutiveHyphenLimit'), qn('w:hyphenationZone'),
|
||||
qn('w:doNotHyphenateCaps'), qn('w:showEnvelope'),
|
||||
qn('w:summaryLength'), qn('w:clickAndTypeStyle'),
|
||||
qn('w:defaultTableStyle'), qn('w:evenAndOddHeaders'),
|
||||
qn('w:bookFoldRevPrinting'), qn('w:bookFoldPrinting'),
|
||||
qn('w:bookFoldPrintingSheets'), qn('w:drawingGridHorizontalSpacing'),
|
||||
qn('w:drawingGridVerticalSpacing'),
|
||||
qn('w:displayHorizontalDrawingGridEvery'),
|
||||
qn('w:displayVerticalDrawingGridEvery'),
|
||||
qn('w:doNotUseMarginsForDrawingGridOrigin'),
|
||||
qn('w:drawingGridHorizontalOrigin'),
|
||||
qn('w:drawingGridVerticalOrigin'), qn('w:doNotShadeFormData'),
|
||||
qn('w:noPunctuationKerning'), qn('w:characterSpacingControl'),
|
||||
qn('w:printTwoOnOne'), qn('w:strictFirstAndLastChars'),
|
||||
qn('w:noLineBreaksAfter'), qn('w:noLineBreaksBefore'),
|
||||
qn('w:savePreviewPicture'), qn('w:doNotValidateAgainstSchema'),
|
||||
qn('w:saveInvalidXml'), qn('w:ignoreMixedContent'),
|
||||
qn('w:alwaysShowPlaceholderText'), qn('w:doNotDemarcateInvalidXml'),
|
||||
qn('w:saveXmlDataOnly'), qn('w:useXSLTWhenSaving'),
|
||||
qn('w:saveThroughXslt'), qn('w:showXMLTags'),
|
||||
qn('w:alwaysMergeEmptyNamespace'), qn('w:updateFields'),
|
||||
qn('w:hdrShapeDefaults'), qn('w:footnotePr'), qn('w:endnotePr'),
|
||||
qn('w:compat'), qn('w:rsids'),
|
||||
'{http://schemas.openxmlformats.org/officeDocument/2006/math}mathPr',
|
||||
qn('w:themeFontLang'), qn('w:clrSchemeMapping'), qn('w:doNotIncludeSubdocsInStats'),
|
||||
qn('w:doNotAutoCompressPictures'), qn('w:forceUpgrade'),
|
||||
qn('w:captions'), qn('w:readModeInkLockDown'), qn('w:smartTagType'),
|
||||
qn('w:shapeDefaults'), qn('w:doNotEmbedSmartTags'),
|
||||
qn('w:decimalSymbol'), qn('w:listSeparator'),
|
||||
]
|
||||
|
||||
|
||||
def _reorder_settings(settings):
|
||||
"""Reorder w:settings children to match the CT_Settings schema sequence.
|
||||
Word rejects documents with settings elements in wrong order."""
|
||||
children = list(settings)
|
||||
ordered = []
|
||||
for tag in SETTINGS_ORDER:
|
||||
for child in children[:]:
|
||||
if child.tag == tag:
|
||||
ordered.append(child)
|
||||
children.remove(child)
|
||||
ordered.extend(children)
|
||||
for child in ordered:
|
||||
settings.append(child)
|
||||
|
||||
|
||||
def format_document(filepath, output_path=None, font_name="Caladea", font_size=11,
|
||||
inner_margin=0.500, trim_size="5.5x8.5", bleed=0.0,
|
||||
author="", imprint="", footer_mode="headers",
|
||||
@@ -1823,6 +1892,9 @@ def format_document(filepath, output_path=None, font_name="Caladea", font_size=1
|
||||
# w:settings per OOXML; Word honors it in modern layout compat mode.
|
||||
if settings.find(qn('w:autoHyphenation')) is None:
|
||||
settings.append(OxmlElement('w:autoHyphenation'))
|
||||
# Word requires w:settings children in strict CT_Settings order, so fix up
|
||||
# the position of the two elements just added (LibreOffice would not care).
|
||||
_reorder_settings(settings)
|
||||
|
||||
for idx, sec in enumerate(secs):
|
||||
sec.page_width = Inches(page_w)
|
||||
|
||||
Reference in New Issue
Block a user