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.
- {exstruct-0.3.1 → exstruct-0.3.2}/PKG-INFO +91 -7
- {exstruct-0.3.1 → exstruct-0.3.2}/README.md +90 -6
- {exstruct-0.3.1 → exstruct-0.3.2}/pyproject.toml +1 -1
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/base.py +5 -1
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/com_backend.py +5 -1
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/openpyxl_backend.py +13 -1
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/cells.py +36 -1
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/integrate.py +3 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/modeling.py +4 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/pipeline.py +54 -1
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/engine.py +9 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/models/__init__.py +13 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/LICENSE +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/__init__.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/cli/availability.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/cli/main.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/__init__.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/backends/__init__.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/charts.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/logging_utils.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/ranges.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/shapes.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/core/workbook.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/errors.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/io/__init__.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/io/serialize.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/models/maps.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/models/types.py +0 -0
- {exstruct-0.3.1 → exstruct-0.3.2}/src/exstruct/py.typed +0 -0
- {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.
|
|
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
|

|
|
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
|
-
##
|
|
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
|
-
"
|
|
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
|
+

|
|
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
|

|
|
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
|
-
##
|
|
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
|
-
"
|
|
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
|
+

|
|
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
|
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|