exstruct 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {exstruct-0.3.1 → exstruct-0.3.2}/PKG-INFO +91 -7
  2. {exstruct-0.3.1 → exstruct-0.3.2}/README.md +90 -6
  3. {exstruct-0.3.1 → exstruct-0.3.2}/pyproject.toml +1 -1
  4. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/base.py +5 -1
  5. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/com_backend.py +5 -1
  6. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/openpyxl_backend.py +13 -1
  7. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/cells.py +36 -1
  8. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/integrate.py +3 -0
  9. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/modeling.py +4 -0
  10. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/pipeline.py +54 -1
  11. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/engine.py +9 -0
  12. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/models/__init__.py +13 -0
  13. {exstruct-0.3.1 → exstruct-0.3.2}/LICENSE +0 -0
  14. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/__init__.py +0 -0
  15. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/cli/availability.py +0 -0
  16. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/cli/main.py +0 -0
  17. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/__init__.py +0 -0
  18. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/__init__.py +0 -0
  19. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/charts.py +0 -0
  20. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/logging_utils.py +0 -0
  21. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/ranges.py +0 -0
  22. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/shapes.py +0 -0
  23. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/workbook.py +0 -0
  24. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/errors.py +0 -0
  25. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/io/__init__.py +0 -0
  26. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/io/serialize.py +0 -0
  27. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/models/maps.py +0 -0
  28. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/models/types.py +0 -0
  29. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/py.typed +0 -0
  30. {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/render/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exstruct
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines
5
5
  Keywords: excel,structure,data,exstruct
6
6
  Author: harumiWeb
@@ -59,14 +59,14 @@ Description-Content-Type: text/markdown
59
59
 
60
60
  ![ExStruct Image](/docs/assets/icon.webp)
61
61
 
62
- ExStruct reads Excel workbooks and outputs structured data (cells, table candidates, shapes, charts, smartart, print areas/views, auto page-break areas, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates + print areas), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
62
+ ExStruct reads Excel workbooks and outputs structured data (cells, table candidates, shapes, charts, smartart, merged cell ranges, print areas/views, auto page-break areas, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates + print areas), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
63
63
 
64
64
  [日本版 README](README.ja.md)
65
65
 
66
66
  ## Features
67
67
 
68
68
  - **Excel → Structured JSON**: cells, shapes, charts, smartart, table candidates, print areas/views, and auto page-break areas per sheet.
69
- - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, print areas), `verbose` (all shapes with width/height, charts with size, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
69
+ - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, merged cell ranges, print areas), `verbose` (all shapes with width/height, charts with size, merged cell ranges, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
70
70
  - **Auto page-break export (COM only)**: capture Excel-computed auto page breaks and write per-area JSON/YAML/TOON when requested (CLI option appears only when COM is available).
71
71
  - **Formats**: JSON (compact by default, `--pretty` available), YAML, TOON (optional dependencies).
72
72
  - **Table detection tuning**: adjust heuristics at runtime via API.
@@ -188,8 +188,8 @@ Use higher thresholds to reduce false positives; lower them if true tables are m
188
188
  ## Output Modes
189
189
 
190
190
  - **light**: cells + table candidates (no COM needed).
191
- - **standard**: texted shapes + arrows, charts (COM if available), table candidates. Hyperlinks are off unless `include_cell_links=True`.
192
- - **verbose**: all shapes (with width/height), charts, table candidates, cell hyperlinks, and `colors_map`.
191
+ - **standard**: texted shapes + arrows, charts (COM if available), merged cell ranges, table candidates. Hyperlinks are off unless `include_cell_links=True`.
192
+ - **verbose**: all shapes (with width/height), charts, merged cell ranges, table candidates, cell hyperlinks, and `colors_map`.
193
193
 
194
194
  ## Error Handling / Fallbacks
195
195
 
@@ -207,7 +207,7 @@ exstruct input.xlsx --pdf --image --dpi 144
207
207
 
208
208
  Creates `<output>.pdf` and `<output>_images/` PNGs per sheet.
209
209
 
210
- ## Benchmark: Excel Structuring Demo
210
+ ## Example 1: Excel Structuring Demo
211
211
 
212
212
  To show how well exstruct can structure Excel, we parse a workbook that combines three elements on one sheet and share an AI reasoning benchmark that uses the JSON output.
213
213
 
@@ -277,6 +277,7 @@ Below is a **shortened JSON output example** from parsing this Excel workbook.
277
277
  "text": "開始",
278
278
  "l": 148,
279
279
  "t": 220,
280
+ "kind": "shape",
280
281
  "type": "AutoShape-FlowchartProcess"
281
282
  },
282
283
  {
@@ -284,12 +285,13 @@ Below is a **shortened JSON output example** from parsing this Excel workbook.
284
285
  "text": "入力データ読み込み",
285
286
  "l": 132,
286
287
  "t": 282,
288
+ "kind": "shape",
287
289
  "type": "AutoShape-FlowchartProcess"
288
290
  },
289
291
  {
290
292
  "l": 193,
291
293
  "t": 246,
292
- "type": "AutoShape-Mixed",
294
+ "kind": "arrow",
293
295
  "begin_arrow_style": 1,
294
296
  "end_arrow_style": 2,
295
297
  "begin_id": 1,
@@ -389,6 +391,87 @@ flowchart TD
389
391
  ```
390
392
  ````
391
393
 
394
+ ## Example 2: General Application Form
395
+
396
+ ### Excel Sheet
397
+
398
+ ![General Application Form Excel](/docs/assets/demo_form.en.png)
399
+
400
+ ### ExStruct JSON
401
+
402
+ (Truncated for brevity)
403
+
404
+ ```json
405
+ {
406
+ "book_name": "ja_form.xlsx",
407
+ "sheets": {
408
+ "Sheet1": {
409
+ "rows": [
410
+ { "r": 1, "c": { "0": "??????????????" } },
411
+ {
412
+ "r": 3,
413
+ "c": { "0": "???", "7": " ???????????????" }
414
+ },
415
+ { "r": 4, "c": { "1": "X???" } },
416
+ ...
417
+ ],
418
+ "table_candidates": ["B25:C26", "C37:D50"],
419
+ "merged_cells": [
420
+ {
421
+ "r1": 55,
422
+ "c1": 5,
423
+ "r2": 55,
424
+ "c2": 10,
425
+ "v": "?????????????????????????????"
426
+ },
427
+ { "r1": 54, "c1": 8, "r2": 54, "c2": 10 },
428
+ { "r1": 51, "c1": 5, "r2": 52, "c2": 6, "v": "????" },
429
+ ...
430
+ ]
431
+ }
432
+ }
433
+ }
434
+ ```
435
+
436
+ ### LLM reconstruction example
437
+
438
+ ```md
439
+ # ??????????????
440
+
441
+ ????????????????????????
442
+ X ??
443
+
444
+ ?????????????????????????????????????????
445
+
446
+ ---
447
+
448
+ ## ??????
449
+
450
+ | ?? | ?? |
451
+ | ------ | -------------- |
452
+ | ???? | |
453
+ | ????? | |
454
+ | ?????? | |
455
+ | ???? | |
456
+ | ???? | ?????????????? |
457
+ | ?? | |
458
+ | ??? | |
459
+
460
+ ---
461
+
462
+ ## ?????????????
463
+
464
+ | ?? | ?? |
465
+ | ----------- | ----- |
466
+ | ??????????? | |
467
+ | ??? | |
468
+ | ???????? | ????? |
469
+
470
+ **???????????????????????????????????????**
471
+
472
+ ...
473
+ ```
474
+
392
475
  From this we can see:
393
476
 
394
477
  **exstruct's JSON is already in a format that AI can read and reason over directly.**
@@ -398,6 +481,7 @@ Other LLM inference samples using this library can be found in the following dir
398
481
  - [Basic Excel](sample/basic/)
399
482
  - [Flowchart](sample/flowchart/)
400
483
  - [Gantt Chart](sample/gantt_chart/)
484
+ - [Application forms with many merged cells](sample/forms_with_many_merged_cells/)
401
485
 
402
486
  ### 4. Summary
403
487
 
@@ -4,14 +4,14 @@
4
4
 
5
5
  ![ExStruct Image](/docs/assets/icon.webp)
6
6
 
7
- ExStruct reads Excel workbooks and outputs structured data (cells, table candidates, shapes, charts, smartart, print areas/views, auto page-break areas, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates + print areas), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
7
+ ExStruct reads Excel workbooks and outputs structured data (cells, table candidates, shapes, charts, smartart, merged cell ranges, print areas/views, auto page-break areas, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates + print areas), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
8
8
 
9
9
  [日本版 README](README.ja.md)
10
10
 
11
11
  ## Features
12
12
 
13
13
  - **Excel → Structured JSON**: cells, shapes, charts, smartart, table candidates, print areas/views, and auto page-break areas per sheet.
14
- - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, print areas), `verbose` (all shapes with width/height, charts with size, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
14
+ - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, merged cell ranges, print areas), `verbose` (all shapes with width/height, charts with size, merged cell ranges, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
15
15
  - **Auto page-break export (COM only)**: capture Excel-computed auto page breaks and write per-area JSON/YAML/TOON when requested (CLI option appears only when COM is available).
16
16
  - **Formats**: JSON (compact by default, `--pretty` available), YAML, TOON (optional dependencies).
17
17
  - **Table detection tuning**: adjust heuristics at runtime via API.
@@ -133,8 +133,8 @@ Use higher thresholds to reduce false positives; lower them if true tables are m
133
133
  ## Output Modes
134
134
 
135
135
  - **light**: cells + table candidates (no COM needed).
136
- - **standard**: texted shapes + arrows, charts (COM if available), table candidates. Hyperlinks are off unless `include_cell_links=True`.
137
- - **verbose**: all shapes (with width/height), charts, table candidates, cell hyperlinks, and `colors_map`.
136
+ - **standard**: texted shapes + arrows, charts (COM if available), merged cell ranges, table candidates. Hyperlinks are off unless `include_cell_links=True`.
137
+ - **verbose**: all shapes (with width/height), charts, merged cell ranges, table candidates, cell hyperlinks, and `colors_map`.
138
138
 
139
139
  ## Error Handling / Fallbacks
140
140
 
@@ -152,7 +152,7 @@ exstruct input.xlsx --pdf --image --dpi 144
152
152
 
153
153
  Creates `<output>.pdf` and `<output>_images/` PNGs per sheet.
154
154
 
155
- ## Benchmark: Excel Structuring Demo
155
+ ## Example 1: Excel Structuring Demo
156
156
 
157
157
  To show how well exstruct can structure Excel, we parse a workbook that combines three elements on one sheet and share an AI reasoning benchmark that uses the JSON output.
158
158
 
@@ -222,6 +222,7 @@ Below is a **shortened JSON output example** from parsing this Excel workbook.
222
222
  "text": "開始",
223
223
  "l": 148,
224
224
  "t": 220,
225
+ "kind": "shape",
225
226
  "type": "AutoShape-FlowchartProcess"
226
227
  },
227
228
  {
@@ -229,12 +230,13 @@ Below is a **shortened JSON output example** from parsing this Excel workbook.
229
230
  "text": "入力データ読み込み",
230
231
  "l": 132,
231
232
  "t": 282,
233
+ "kind": "shape",
232
234
  "type": "AutoShape-FlowchartProcess"
233
235
  },
234
236
  {
235
237
  "l": 193,
236
238
  "t": 246,
237
- "type": "AutoShape-Mixed",
239
+ "kind": "arrow",
238
240
  "begin_arrow_style": 1,
239
241
  "end_arrow_style": 2,
240
242
  "begin_id": 1,
@@ -334,6 +336,87 @@ flowchart TD
334
336
  ```
335
337
  ````
336
338
 
339
+ ## Example 2: General Application Form
340
+
341
+ ### Excel Sheet
342
+
343
+ ![General Application Form Excel](/docs/assets/demo_form.en.png)
344
+
345
+ ### ExStruct JSON
346
+
347
+ (Truncated for brevity)
348
+
349
+ ```json
350
+ {
351
+ "book_name": "ja_form.xlsx",
352
+ "sheets": {
353
+ "Sheet1": {
354
+ "rows": [
355
+ { "r": 1, "c": { "0": "??????????????" } },
356
+ {
357
+ "r": 3,
358
+ "c": { "0": "???", "7": " ???????????????" }
359
+ },
360
+ { "r": 4, "c": { "1": "X???" } },
361
+ ...
362
+ ],
363
+ "table_candidates": ["B25:C26", "C37:D50"],
364
+ "merged_cells": [
365
+ {
366
+ "r1": 55,
367
+ "c1": 5,
368
+ "r2": 55,
369
+ "c2": 10,
370
+ "v": "?????????????????????????????"
371
+ },
372
+ { "r1": 54, "c1": 8, "r2": 54, "c2": 10 },
373
+ { "r1": 51, "c1": 5, "r2": 52, "c2": 6, "v": "????" },
374
+ ...
375
+ ]
376
+ }
377
+ }
378
+ }
379
+ ```
380
+
381
+ ### LLM reconstruction example
382
+
383
+ ```md
384
+ # ??????????????
385
+
386
+ ????????????????????????
387
+ X ??
388
+
389
+ ?????????????????????????????????????????
390
+
391
+ ---
392
+
393
+ ## ??????
394
+
395
+ | ?? | ?? |
396
+ | ------ | -------------- |
397
+ | ???? | |
398
+ | ????? | |
399
+ | ?????? | |
400
+ | ???? | |
401
+ | ???? | ?????????????? |
402
+ | ?? | |
403
+ | ??? | |
404
+
405
+ ---
406
+
407
+ ## ?????????????
408
+
409
+ | ?? | ?? |
410
+ | ----------- | ----- |
411
+ | ??????????? | |
412
+ | ??? | |
413
+ | ???????? | ????? |
414
+
415
+ **???????????????????????????????????????**
416
+
417
+ ...
418
+ ```
419
+
337
420
  From this we can see:
338
421
 
339
422
  **exstruct's JSON is already in a format that AI can read and reason over directly.**
@@ -343,6 +426,7 @@ Other LLM inference samples using this library can be found in the following dir
343
426
  - [Basic Excel](sample/basic/)
344
427
  - [Flowchart](sample/flowchart/)
345
428
  - [Gantt Chart](sample/gantt_chart/)
429
+ - [Application forms with many merged cells](sample/forms_with_many_merged_cells/)
346
430
 
347
431
  ### 4. Summary
348
432
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -3,11 +3,12 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Protocol
5
5
 
6
- from ...models import CellRow, PrintArea
6
+ from ...models import CellRow, MergedCell, PrintArea
7
7
  from ..cells import WorkbookColorsMap
8
8
 
9
9
  CellData = dict[str, list[CellRow]]
10
10
  PrintAreaData = dict[str, list[PrintArea]]
11
+ MergedCellData = dict[str, list[MergedCell]]
11
12
 
12
13
 
13
14
  @dataclass(frozen=True)
@@ -36,3 +37,6 @@ class Backend(Protocol):
36
37
  self, *, include_default_background: bool, ignore_colors: set[str] | None
37
38
  ) -> WorkbookColorsMap | None:
38
39
  """Extract colors map from the workbook."""
40
+
41
+ def extract_merged_cells(self) -> MergedCellData:
42
+ """Extract merged cell ranges from the workbook."""
@@ -11,7 +11,7 @@ import xlwings as xw
11
11
  from ...models import PrintArea
12
12
  from ..cells import WorkbookColorsMap, extract_sheet_colors_map_com
13
13
  from ..ranges import parse_range_zero_based
14
- from .base import PrintAreaData
14
+ from .base import MergedCellData, PrintAreaData
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
17
 
@@ -154,6 +154,10 @@ class ComBackend:
154
154
  continue
155
155
  return results
156
156
 
157
+ def extract_merged_cells(self) -> MergedCellData:
158
+ """Extract merged cell ranges via COM (not implemented)."""
159
+ raise NotImplementedError("COM merged cell extraction is not implemented.")
160
+
157
161
 
158
162
  def _parse_print_area_range(range_str: str) -> tuple[int, int, int, int] | None:
159
163
  """Parse an Excel range string into zero-based coordinates.
@@ -13,10 +13,11 @@ from ..cells import (
13
13
  extract_sheet_cells,
14
14
  extract_sheet_cells_with_links,
15
15
  extract_sheet_colors_map,
16
+ extract_sheet_merged_cells,
16
17
  )
17
18
  from ..ranges import parse_range_zero_based
18
19
  from ..workbook import openpyxl_workbook
19
- from .base import CellData, PrintAreaData
20
+ from .base import CellData, MergedCellData, PrintAreaData
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
@@ -87,6 +88,17 @@ class OpenpyxlBackend:
87
88
  )
88
89
  return None
89
90
 
91
+ def extract_merged_cells(self) -> MergedCellData:
92
+ """Extract merged cell ranges per sheet.
93
+
94
+ Returns:
95
+ Mapping of sheet name to merged cell ranges.
96
+ """
97
+ try:
98
+ return extract_sheet_merged_cells(self.file_path)
99
+ except Exception:
100
+ return {}
101
+
90
102
  def detect_tables(self, sheet_name: str) -> list[str]:
91
103
  """Detect table candidates for a single sheet.
92
104
 
@@ -15,7 +15,7 @@ from openpyxl.worksheet.worksheet import Worksheet
15
15
  import pandas as pd
16
16
  import xlwings as xw
17
17
 
18
- from ..models import CellRow
18
+ from ..models import CellRow, MergedCell
19
19
  from .workbook import openpyxl_workbook
20
20
 
21
21
  logger = logging.getLogger(__name__)
@@ -526,6 +526,41 @@ def extract_sheet_cells_with_links(file_path: Path) -> dict[str, list[CellRow]]:
526
526
  return merged
527
527
 
528
528
 
529
+ def extract_sheet_merged_cells(file_path: Path) -> dict[str, list[MergedCell]]:
530
+ """Extract merged cell ranges per sheet via openpyxl.
531
+
532
+ Args:
533
+ file_path: Excel workbook path.
534
+
535
+ Returns:
536
+ Mapping of sheet name to merged cell ranges.
537
+ """
538
+ merged_by_sheet: dict[str, list[MergedCell]] = {}
539
+ with openpyxl_workbook(file_path, data_only=True, read_only=False) as wb:
540
+ for ws in wb.worksheets:
541
+ merged_ranges = getattr(ws, "merged_cells", None)
542
+ if merged_ranges is None:
543
+ merged_by_sheet[ws.title] = []
544
+ continue
545
+ results: list[MergedCell] = []
546
+ for merged_range in getattr(merged_ranges, "ranges", []):
547
+ bounds = range_boundaries(str(merged_range))
548
+ min_col, min_row, max_col, max_row = bounds
549
+ cell_value = ws.cell(row=min_row, column=min_col).value
550
+ value_str = "" if cell_value is None else str(cell_value)
551
+ results.append(
552
+ MergedCell(
553
+ r1=min_row,
554
+ c1=min_col - 1,
555
+ r2=max_row,
556
+ c2=max_col - 1,
557
+ v=value_str,
558
+ )
559
+ )
560
+ merged_by_sheet[ws.title] = results
561
+ return merged_by_sheet
562
+
563
+
529
564
  def shrink_to_content( # noqa: C901
530
565
  sheet: xw.Sheet,
531
566
  top: int,
@@ -17,6 +17,7 @@ def extract_workbook( # noqa: C901
17
17
  include_colors_map: bool | None = None,
18
18
  include_default_background: bool = False,
19
19
  ignore_colors: set[str] | None = None,
20
+ include_merged_cells: bool | None = None,
20
21
  ) -> WorkbookData:
21
22
  """Extract workbook and return WorkbookData.
22
23
 
@@ -31,6 +32,7 @@ def extract_workbook( # noqa: C901
31
32
  include_colors_map: Whether to include colors map; None uses mode defaults.
32
33
  include_default_background: Whether to include default background color.
33
34
  ignore_colors: Optional set of color keys to ignore.
35
+ include_merged_cells: Whether to include merged cell ranges; None uses mode defaults.
34
36
 
35
37
  Returns:
36
38
  Extracted WorkbookData.
@@ -47,6 +49,7 @@ def extract_workbook( # noqa: C901
47
49
  include_colors_map=include_colors_map,
48
50
  include_default_background=include_default_background,
49
51
  ignore_colors=ignore_colors,
52
+ include_merged_cells=include_merged_cells,
50
53
  )
51
54
  result = run_extraction_pipeline(inputs)
52
55
  return result.workbook
@@ -6,6 +6,7 @@ from ..models import (
6
6
  Arrow,
7
7
  CellRow,
8
8
  Chart,
9
+ MergedCell,
9
10
  PrintArea,
10
11
  Shape,
11
12
  SheetData,
@@ -26,6 +27,7 @@ class SheetRawData:
26
27
  print_areas: Extracted print areas.
27
28
  auto_print_areas: Extracted auto page-break areas.
28
29
  colors_map: Mapping of color keys to (row, column) positions.
30
+ merged_cells: Extracted merged cell ranges.
29
31
  """
30
32
 
31
33
  rows: list[CellRow]
@@ -35,6 +37,7 @@ class SheetRawData:
35
37
  print_areas: list[PrintArea]
36
38
  auto_print_areas: list[PrintArea]
37
39
  colors_map: dict[str, list[tuple[int, int]]]
40
+ merged_cells: list[MergedCell]
38
41
 
39
42
 
40
43
  @dataclass(frozen=True)
@@ -67,6 +70,7 @@ def build_sheet_data(raw: SheetRawData) -> SheetData:
67
70
  print_areas=raw.print_areas,
68
71
  auto_print_areas=raw.auto_print_areas,
69
72
  colors_map=raw.colors_map,
73
+ merged_cells=raw.merged_cells,
70
74
  )
71
75
 
72
76
 
@@ -10,7 +10,16 @@ from typing import Literal
10
10
  import xlwings as xw
11
11
 
12
12
  from ..errors import FallbackReason
13
- from ..models import Arrow, CellRow, Chart, PrintArea, Shape, SmartArt, WorkbookData
13
+ from ..models import (
14
+ Arrow,
15
+ CellRow,
16
+ Chart,
17
+ MergedCell,
18
+ PrintArea,
19
+ Shape,
20
+ SmartArt,
21
+ WorkbookData,
22
+ )
14
23
  from .backends.com_backend import ComBackend
15
24
  from .backends.openpyxl_backend import OpenpyxlBackend
16
25
  from .cells import WorkbookColorsMap, detect_tables
@@ -23,6 +32,7 @@ from .workbook import xlwings_workbook
23
32
  ExtractionMode = Literal["light", "standard", "verbose"]
24
33
  CellData = dict[str, list[CellRow]]
25
34
  PrintAreaData = dict[str, list[PrintArea]]
35
+ MergedCellData = dict[str, list[MergedCell]]
26
36
  ShapeData = dict[str, list[Shape | Arrow | SmartArt]]
27
37
  ChartData = dict[str, list[Chart]]
28
38
 
@@ -42,6 +52,7 @@ class ExtractionInputs:
42
52
  include_colors_map: Whether to include background colors map.
43
53
  include_default_background: Whether to include default background color.
44
54
  ignore_colors: Optional set of color keys to ignore.
55
+ include_merged_cells: Whether to include merged cell ranges.
45
56
  """
46
57
 
47
58
  file_path: Path
@@ -52,6 +63,7 @@ class ExtractionInputs:
52
63
  include_colors_map: bool
53
64
  include_default_background: bool
54
65
  ignore_colors: set[str] | None
66
+ include_merged_cells: bool
55
67
 
56
68
 
57
69
  @dataclass
@@ -65,6 +77,7 @@ class ExtractionArtifacts:
65
77
  colors_map_data: Extracted colors map for workbook sheets.
66
78
  shape_data: Extracted shapes per sheet.
67
79
  chart_data: Extracted charts per sheet.
80
+ merged_cell_data: Extracted merged cell ranges per sheet.
68
81
  """
69
82
 
70
83
  cell_data: CellData = field(default_factory=dict)
@@ -73,6 +86,7 @@ class ExtractionArtifacts:
73
86
  colors_map_data: WorkbookColorsMap | None = None
74
87
  shape_data: ShapeData = field(default_factory=dict)
75
88
  chart_data: ChartData = field(default_factory=dict)
89
+ merged_cell_data: MergedCellData = field(default_factory=dict)
76
90
 
77
91
 
78
92
  ExtractionStep = Callable[[ExtractionInputs, ExtractionArtifacts], None]
@@ -164,6 +178,7 @@ def resolve_extraction_inputs(
164
178
  include_colors_map: bool | None,
165
179
  include_default_background: bool,
166
180
  ignore_colors: set[str] | None,
181
+ include_merged_cells: bool | None,
167
182
  ) -> ExtractionInputs:
168
183
  """Resolve include flags and normalize inputs for the pipeline.
169
184
 
@@ -176,6 +191,7 @@ def resolve_extraction_inputs(
176
191
  include_colors_map: Whether to include background colors; None uses mode defaults.
177
192
  include_default_background: Include default background colors when colors_map is enabled.
178
193
  ignore_colors: Optional set of colors to ignore when colors_map is enabled.
194
+ include_merged_cells: Whether to include merged cell ranges; None uses mode defaults.
179
195
 
180
196
  Returns:
181
197
  Resolved ExtractionInputs.
@@ -203,6 +219,9 @@ def resolve_extraction_inputs(
203
219
  resolved_ignore_colors = ignore_colors if resolved_colors_map else None
204
220
  if resolved_colors_map and resolved_ignore_colors is None:
205
221
  resolved_ignore_colors = set()
222
+ resolved_merged_cells = (
223
+ include_merged_cells if include_merged_cells is not None else mode != "light"
224
+ )
206
225
 
207
226
  return ExtractionInputs(
208
227
  file_path=normalized_file_path,
@@ -213,6 +232,7 @@ def resolve_extraction_inputs(
213
232
  include_colors_map=resolved_colors_map,
214
233
  include_default_background=resolved_default_background,
215
234
  ignore_colors=resolved_ignore_colors,
235
+ include_merged_cells=resolved_merged_cells,
216
236
  )
217
237
 
218
238
 
@@ -258,6 +278,11 @@ def build_pre_com_pipeline(inputs: ExtractionInputs) -> list[ExtractionStep]:
258
278
  step=step_extract_colors_map_openpyxl,
259
279
  enabled=lambda _inputs: _inputs.include_colors_map,
260
280
  ),
281
+ StepConfig(
282
+ name="merged_cells_openpyxl",
283
+ step=step_extract_merged_cells_openpyxl,
284
+ enabled=lambda _inputs: _inputs.include_merged_cells,
285
+ ),
261
286
  ),
262
287
  "standard": (
263
288
  StepConfig(
@@ -276,6 +301,11 @@ def build_pre_com_pipeline(inputs: ExtractionInputs) -> list[ExtractionStep]:
276
301
  enabled=lambda _inputs: _inputs.include_colors_map
277
302
  and bool(os.getenv("SKIP_COM_TESTS")),
278
303
  ),
304
+ StepConfig(
305
+ name="merged_cells_openpyxl",
306
+ step=step_extract_merged_cells_openpyxl,
307
+ enabled=lambda _inputs: _inputs.include_merged_cells,
308
+ ),
279
309
  ),
280
310
  "verbose": (
281
311
  StepConfig(
@@ -294,6 +324,11 @@ def build_pre_com_pipeline(inputs: ExtractionInputs) -> list[ExtractionStep]:
294
324
  enabled=lambda _inputs: _inputs.include_colors_map
295
325
  and bool(os.getenv("SKIP_COM_TESTS")),
296
326
  ),
327
+ StepConfig(
328
+ name="merged_cells_openpyxl",
329
+ step=step_extract_merged_cells_openpyxl,
330
+ enabled=lambda _inputs: _inputs.include_merged_cells,
331
+ ),
297
332
  ),
298
333
  }
299
334
  steps: list[ExtractionStep] = []
@@ -432,6 +467,19 @@ def step_extract_colors_map_openpyxl(
432
467
  )
433
468
 
434
469
 
470
+ def step_extract_merged_cells_openpyxl(
471
+ inputs: ExtractionInputs, artifacts: ExtractionArtifacts
472
+ ) -> None:
473
+ """Extract merged cell ranges via openpyxl.
474
+
475
+ Args:
476
+ inputs: Pipeline inputs.
477
+ artifacts: Artifact container to update.
478
+ """
479
+ backend = OpenpyxlBackend(inputs.file_path)
480
+ artifacts.merged_cell_data = backend.extract_merged_cells()
481
+
482
+
435
483
  def step_extract_shapes_com(
436
484
  inputs: ExtractionInputs, artifacts: ExtractionArtifacts, workbook: xw.Book
437
485
  ) -> None:
@@ -540,6 +588,7 @@ def collect_sheet_raw_data(
540
588
  cell_data: CellData,
541
589
  shape_data: ShapeData,
542
590
  chart_data: ChartData,
591
+ merged_cell_data: MergedCellData,
543
592
  workbook: xw.Book,
544
593
  mode: ExtractionMode = "standard",
545
594
  print_area_data: PrintAreaData | None = None,
@@ -552,6 +601,7 @@ def collect_sheet_raw_data(
552
601
  cell_data: Extracted cell rows per sheet.
553
602
  shape_data: Extracted shapes per sheet.
554
603
  chart_data: Extracted charts per sheet.
604
+ merged_cell_data: Extracted merged cells per sheet.
555
605
  workbook: xlwings workbook instance.
556
606
  mode: Extraction mode.
557
607
  print_area_data: Optional print area data per sheet.
@@ -574,6 +624,7 @@ def collect_sheet_raw_data(
574
624
  if auto_page_break_data
575
625
  else [],
576
626
  colors_map=_resolve_sheet_colors_map(colors_map_data, sheet_name),
627
+ merged_cells=merged_cell_data.get(sheet_name, []),
577
628
  )
578
629
  result[sheet_name] = sheet_raw
579
630
  return result
@@ -620,6 +671,7 @@ def run_extraction_pipeline(inputs: ExtractionInputs) -> PipelineResult:
620
671
  cell_data=artifacts.cell_data,
621
672
  shape_data=artifacts.shape_data,
622
673
  chart_data=artifacts.chart_data,
674
+ merged_cell_data=artifacts.merged_cell_data,
623
675
  workbook=workbook,
624
676
  mode=inputs.mode,
625
677
  print_area_data=artifacts.print_area_data
@@ -691,6 +743,7 @@ def build_cells_tables_workbook(
691
743
  else [],
692
744
  auto_print_areas=[],
693
745
  colors_map=sheet_colors.colors_map if sheet_colors else {},
746
+ merged_cells=artifacts.merged_cell_data.get(sheet_name, []),
694
747
  )
695
748
  raw = WorkbookRawData(book_name=inputs.file_path.name, sheets=sheets)
696
749
  return build_workbook_data(raw)
@@ -70,6 +70,7 @@ class StructOptions:
70
70
  before extraction. Use this to tweak table detection heuristics
71
71
  per engine instance without touching global state.
72
72
  include_colors_map: Whether to extract background color maps.
73
+ include_merged_cells: Whether to extract merged cell ranges.
73
74
  colors: Color extraction options.
74
75
  """
75
76
 
@@ -79,6 +80,7 @@ class StructOptions:
79
80
  )
80
81
  include_cell_links: bool | None = None # None -> auto: verbose=True, others=False
81
82
  include_colors_map: bool | None = None # None -> auto: verbose=True, others=False
83
+ include_merged_cells: bool | None = None # None -> auto: light=False, others=True
82
84
  colors: ColorsOptions = field(default_factory=ColorsOptions)
83
85
 
84
86
 
@@ -121,6 +123,9 @@ class FilterOptions(BaseModel):
121
123
  include_auto_print_areas: bool = Field(
122
124
  default=False, description="Include COM-computed auto page-break areas."
123
125
  )
126
+ include_merged_cells: bool = Field(
127
+ default=True, description="Include merged cell ranges."
128
+ )
124
129
 
125
130
 
126
131
  class DestinationOptions(BaseModel):
@@ -279,6 +284,9 @@ class ExStructEngine:
279
284
  colors_map=sheet.colors_map,
280
285
  print_areas=sheet.print_areas if include_print_areas else [],
281
286
  auto_print_areas=sheet.auto_print_areas if include_auto_print_areas else [],
287
+ merged_cells=sheet.merged_cells
288
+ if self.output.filters.include_merged_cells
289
+ else [],
282
290
  )
283
291
 
284
292
  def _filter_workbook(
@@ -348,6 +356,7 @@ class ExStructEngine:
348
356
  include_colors_map=self.options.include_colors_map,
349
357
  include_default_background=self.options.colors.include_default_background,
350
358
  ignore_colors=self.options.colors.ignore_colors_set(),
359
+ include_merged_cells=self.options.include_merged_cells,
351
360
  )
352
361
 
353
362
  def serialize(
@@ -76,6 +76,16 @@ class SmartArt(BaseShape):
76
76
  )
77
77
 
78
78
 
79
+ class MergedCell(BaseModel):
80
+ """Metadata for a merged cell range."""
81
+
82
+ r1: int = Field(description="Start row (1-based).")
83
+ c1: int = Field(description="Start column (0-based).")
84
+ r2: int = Field(description="End row (1-based, inclusive).")
85
+ c2: int = Field(description="End column (0-based, inclusive).")
86
+ v: str = ""
87
+
88
+
79
89
  class CellRow(BaseModel):
80
90
  """A single row of cells with optional hyperlinks."""
81
91
 
@@ -160,6 +170,9 @@ class SheetData(BaseModel):
160
170
  "where row is 1-based and column is 0-based."
161
171
  ),
162
172
  )
173
+ merged_cells: list[MergedCell] = Field(
174
+ default_factory=list, description="Merged cell ranges on the sheet."
175
+ )
163
176
 
164
177
  def _as_payload(self) -> dict[str, object]:
165
178
  from ..io import dict_without_empty_values
File without changes
File without changes