DOCX Pagination and Page Index Assignment#
Unlike PDFs — which have explicit, renderer-defined page boundaries — DOCX files have no inherent page structure in their XML. MinerU simulates pagination by splitting content at section breaks (w:sectPr) during parsing, then assigns sequential page indices when converting raw output into the middle JSON format.
Page Boundary Detection: w:sectPr-Based Splitting#
The DocxConverter in mineru/model/docx/docx_converter.py maintains two state variables to manage page splitting :
self.pages: list— the list of all pages (each page is itself a list of block dicts)self.cur_page: list— the currently accumulating page
The initial page is seeded before traversal begins . _walk_linear() then iterates over every top-level body element, dispatching to handlers that append blocks to self.cur_page.
Section break detection in _handle_text_elements()#
_handle_text_elements() checks each paragraph element for a nested w:sectPr :
- Empty paragraph with
w:sectPr→ calls_start_new_page()immediately (before processing), resettingcur_pageto a new list and appending it topages. - Paragraph with text and
w:sectPr→ setsis_section_end = True, processes the paragraph's content first, then calls_start_new_page()at the end . This ensures the last paragraph before the section break lands on the correct page.
Layout-only section breaks are skipped#
_is_layout_only_section_break() gates the page-split logic. A w:sectPr is treated as layout-only (no new page) when all of the following hold:
- The paragraph contains no visible text or images
- The section type is
continuous - All
w:pgMarattributes (header,footer,top,bottom,left,right) are"0"
This prevents margin-adjustment-only continuous sections from spuriously creating empty pages .
Page Index Assignment: result_to_middle_json()#
DocxConverter.convert() returns self.pages via convert_binary(), passing a list-of-lists where each inner list is one page's blocks. The downstream office_docx_analyze() feeds this directly to result_to_middle_json():
office_docx_analyze()
→ convert_binary() # returns converter.pages
→ result_to_middle_json() # assigns page_idx for each page
Inside result_to_middle_json(), page index assignment is straightforward :
for index, page_blocks in enumerate(model_output_blocks_list):
page_info = blocks_to_page_info(page_blocks, image_writer, index)
middle_json["pdf_info"].append(page_info)
The index from Python's enumerate() becomes the page_idx field in each page's info dict . Pages are 0-indexed and ordered exactly as they appear in converter.pages, which mirrors document order.
blocks_to_page_info() wraps each page's blocks through MagicModel (which categorizes blocks by type) and produces the final page_info dict with para_blocks, discarded_blocks, and page_idx .
Data Flow Summary#
DOCX XML (w:body)
↓ _walk_linear() / _handle_text_elements()
↓ w:sectPr → _start_new_page()
converter.pages = [[blocks_p0], [blocks_p1], ...]
↓ convert_binary() → office_docx_analyze()
↓ result_to_middle_json()
middle_json["pdf_info"] = [
{"page_idx": 0, "para_blocks": [...], ...},
{"page_idx": 1, "para_blocks": [...], ...},
...
]
The middle JSON format uses pdf_info as the page array key regardless of file type — DOCX output reuses the same schema as the PDF pipeline, enabling unified downstream processing .
Key Source Files#
| File | Role |
|---|---|
mineru/model/docx/docx_converter.py | w:sectPr detection, _start_new_page(), block accumulation |
mineru/model/docx/main.py | convert_binary() — returns converter.pages |
mineru/backend/office/docx_analyze.py | office_docx_analyze() — entry point calling both converter and middle JSON builder |
mineru/backend/office/model_output_to_middle_json.py | result_to_middle_json() — assigns page_idx via enumerate() |
This mechanism was introduced in PR #4349, which replaced a single flat self.blocks accumulator with the per-page self.pages/self.cur_page model and added section header/footer extraction.