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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
144
tests/make_bisect_files.py
Normal 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()
|
||||
Reference in New Issue
Block a user