Add Word bisection fixture batch for full-book open failure

Local audit of the full-book output is now exhaustive and clean (package
parts, content types, rels, styles, numbering, bookmark ids, fields,
sectPr/settings ordering, control chars, header/footer machine). LibreOffice
renders the file fine but Word hard-refuses it, so the trigger must be
isolated empirically: each candidate removes exactly one feature family.

- tests/make_bisect_files.py: regenerates six candidates from the final
  full-book output (no running heads; no oddPage/titlePg; no field runs;
  no style refs; first 8 sections only; no images).
- tests/word_bisect/bisect_0[1-6]_*.docx: the six fixtures (all verified to
  load via python-docx and convert via LibreOffice).
This commit is contained in:
bm
2026-09-23 07:43:58 -07:00
parent 9e35f24627
commit 0031acf59e
2 changed files with 145 additions and 0 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ umbraformat/formatted/
# Documents / user scratch files (not part of the app source)
*.docx
!tests/*.docx
!tests/word_bisect/*.docx
summary.txt
Sections_Tags
Word_to_PDF_print_formatting

144
tests/make_bisect_files.py Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python
"""Generate Word-diagnostic bisection files from the real full-book output.
Word refuses the full-book output with a hard "Word experienced an error
trying to open the file" dialog while the mini-book fixture opens fine, and
every structural OOXML audit on the full file passes. LibreOffice renders all
of these files, so Word itself is the only oracle that can tell them apart.
Each candidate starts from the final full-book DOCX and removes exactly one
feature family:
01 no_running - every sectPr headerReference/footerReference removed
02 no_oddpage - sectPr w:type -> nextPage, titlePg dropped, evenAndOddHeaders off
03 no_fields - all fldChar/instrText runs removed (TOC PAGEREF machinery)
04 no_styles - all w:pStyle / w:rStyle removed (style-system interaction)
05 first8 - body truncated after the 8th section boundary
06 no_images - drawing paragraphs removed
Open each in Word; whichever show the error dialog pinpoint the feature
family at fault.
"""
import os
import sys
from docx import Document
from docx.oxml.ns import qn
from lxml import etree # noqa: F401 (kept for parity with the other makers)
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_BASE = os.path.normpath(os.path.join(
HERE, '..', 'umbraformat', 'formatted',
'3a7d086e-c5ab-47bc-a3c5-0ad6352bd9ae_formatted.docx'))
OUTDIR = os.path.join(HERE, 'word_bisect')
NAMES = [
'bisect_01_no_running.docx',
'bisect_02_no_oddpage.docx',
'bisect_03_no_fields.docx',
'bisect_04_no_styles.docx',
'bisect_05_first8.docx',
'bisect_06_no_images.docx',
]
def _save(doc, name):
os.makedirs(OUTDIR, exist_ok=True)
path = os.path.join(OUTDIR, name)
doc.save(path)
return path
def make_no_running(doc):
"""Remove header/footer references from every section (incl. first-page)."""
for sectPr in doc.element.body.iter(qn('w:sectPr')):
for tag in ('w:headerReference', 'w:footerReference'):
for el in sectPr.findall(qn(tag)):
sectPr.remove(el)
def make_no_oddpage(doc):
"""Neutralise the oddPage section-start / odd-even running machinery."""
for sectPr in doc.element.body.iter(qn('w:sectPr')):
t = sectPr.find(qn('w:type'))
if t is not None:
t.set(qn('w:val'), 'nextPage')
tp = sectPr.find(qn('w:titlePg'))
if tp is not None:
sectPr.remove(tp)
settings = doc.settings.element
for el in settings.findall(qn('w:evenAndOddHeaders')):
settings.remove(el)
def make_no_fields(doc):
"""Drop every run carrying field code/result machinery."""
for p in doc.element.body.iter(qn('w:p')):
for r in list(p.findall(qn('w:r'))):
if r.find(qn('w:fldChar')) is not None or r.find(qn('w:instrText')) is not None:
p.remove(r)
def make_no_styles(doc):
"""Remove style references from all paragraphs/runs (renders as Normal)."""
for el in list(doc.element.body.iter()):
if el.tag in (qn('w:pStyle'), qn('w:rStyle')):
el.getparent().remove(el)
def make_first8(doc):
"""Truncate the body after the 8th section boundary (move its sectPr to
body level and drop every later sibling, incl. all later sections)."""
body = doc.element.body
markers = []
for s in body.iter(qn('w:sectPr')):
parent = s.getparent()
if parent is not None and parent.tag == qn('w:pPr'):
markers.append((s, parent.getparent()))
sect, para = markers[7] # 8th boundary (0-based)
sect.getparent().remove(sect)
nxt = para.getnext()
while nxt is not None:
nxt2 = nxt.getnext()
body.remove(nxt)
nxt = nxt2
for fin in body.findall(qn('w:sectPr')):
body.remove(fin)
body.append(sect)
def make_no_images(doc):
"""Remove paragraphs containing inline drawings."""
for p in list(doc.element.body.iter(qn('w:p'))):
if p.find(qn('w:drawing')) is not None:
p.getparent().remove(p)
MAKERS = [
make_no_running,
make_no_oddpage,
make_no_fields,
make_no_styles,
make_first8,
make_no_images,
]
def main():
base = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_BASE
if not os.path.exists(base):
sys.exit(f"base output not found: {base}\n"
"Regenerate it via the API first, or pass its path as argv[1].")
for maker, name in zip(MAKERS, NAMES):
doc = Document(base)
maker(doc)
path = _save(doc, name)
# sanity: it must still open via python-docx
Document(path)
print(f"wrote {path}")
if __name__ == '__main__':
main()