exstruct 0.2.70__tar.gz → 0.2.80__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.2.70 → exstruct-0.2.80}/PKG-INFO +18 -4
- {exstruct-0.2.70 → exstruct-0.2.80}/README.md +17 -3
- {exstruct-0.2.70 → exstruct-0.2.80}/pyproject.toml +1 -1
- exstruct-0.2.80/src/exstruct/cli/availability.py +49 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/cli/main.py +38 -1
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/core/shapes.py +109 -67
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/models/__init__.py +15 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/LICENSE +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/__init__.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/core/__init__.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/core/cells.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/core/charts.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/core/integrate.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/engine.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/errors.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/io/__init__.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/models/maps.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/models/types.py +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/py.typed +0 -0
- {exstruct-0.2.70 → exstruct-0.2.80}/src/exstruct/render/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: exstruct
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.80
|
|
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
|
|
@@ -67,7 +67,7 @@ ExStruct reads Excel workbooks and outputs structured data (cells, table candida
|
|
|
67
67
|
|
|
68
68
|
- **Excel → Structured JSON**: cells, shapes, charts, table candidates, print areas/views, and auto page-break areas per sheet.
|
|
69
69
|
- **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, print areas), `verbose` (all shapes with width/height, charts with size, print areas). Verbose also emits cell hyperlinks. Size output is flag-controlled.
|
|
70
|
-
- **Auto page-break export (COM only)**: capture Excel-computed auto page breaks and write per-area JSON/YAML/TOON when requested.
|
|
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.
|
|
73
73
|
- **CLI rendering** (Excel required): optional PDF and per-sheet PNGs.
|
|
@@ -99,11 +99,12 @@ exstruct input.xlsx --format yaml # YAML (needs pyyaml)
|
|
|
99
99
|
exstruct input.xlsx --format toon # TOON (needs python-toon)
|
|
100
100
|
exstruct input.xlsx --sheets-dir sheets/ # split per sheet in chosen format
|
|
101
101
|
exstruct input.xlsx --print-areas-dir areas/ # split per print area (if any)
|
|
102
|
+
exstruct input.xlsx --auto-page-breaks-dir auto_areas/ # COM only; option appears when available
|
|
102
103
|
exstruct input.xlsx --mode light # cells + table candidates only
|
|
103
104
|
exstruct input.xlsx --pdf --image # PDF and PNGs (Excel required)
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
Auto page-break exports are API
|
|
107
|
+
Auto page-break exports are available via API and CLI when Excel/COM is available; the CLI exposes `--auto-page-breaks-dir` only in COM-capable environments.
|
|
107
108
|
|
|
108
109
|
## Quick Start (Python)
|
|
109
110
|
|
|
@@ -404,12 +405,24 @@ In short, **exstruct = “an engine that converts Excel into a format AI can und
|
|
|
404
405
|
- Default JSON is compact to reduce tokens; use `--pretty` or `pretty=True` when readability matters.
|
|
405
406
|
- Field `table_candidates` replaces `tables`; adjust downstream consumers accordingly.
|
|
406
407
|
|
|
408
|
+
## Enterprise Use
|
|
409
|
+
|
|
410
|
+
ExStruct is used primarily as a **library**, not a service.
|
|
411
|
+
|
|
412
|
+
- No official support or SLA is provided
|
|
413
|
+
- Long-term stability is prioritized over rapid feature growth
|
|
414
|
+
- Forking and internal modification are expected in enterprise use
|
|
415
|
+
|
|
416
|
+
This project is suitable for teams that:
|
|
417
|
+
- need transparency over black-box tools
|
|
418
|
+
- are comfortable maintaining internal forks if necessary
|
|
419
|
+
|
|
407
420
|
## Print Areas and Auto Page Breaks (PrintArea / PrintAreaView)
|
|
408
421
|
|
|
409
422
|
- `SheetData.print_areas` holds print areas (cell coordinates) in light/standard/verbose.
|
|
410
423
|
- `SheetData.auto_print_areas` holds Excel COM-computed auto page-break areas when auto page-break extraction is enabled (COM only).
|
|
411
424
|
- Use `export_print_areas_as(...)` or CLI `--print-areas-dir` to write one file per print area (nothing is written if none exist).
|
|
412
|
-
- Use `DestinationOptions.auto_page_breaks_dir` (preferred) or `export_auto_page_breaks(...)` to write per-auto-page-break files; the API raises `ValueError` if no auto page breaks exist.
|
|
425
|
+
- Use CLI `--auto-page-breaks-dir` (COM only), `DestinationOptions.auto_page_breaks_dir` (preferred), or `export_auto_page_breaks(...)` to write per-auto-page-break files; the API raises `ValueError` if no auto page breaks exist.
|
|
413
426
|
- `PrintAreaView` includes rows and table candidates inside the area, plus shapes/charts that overlap the area (size-less shapes are treated as points). `normalize=True` rebases row/col indices to the area origin.
|
|
414
427
|
|
|
415
428
|
## License
|
|
@@ -419,3 +432,4 @@ BSD-3-Clause. See `LICENSE` for details.
|
|
|
419
432
|
## Documentation
|
|
420
433
|
|
|
421
434
|
- API Reference (GitHub Pages): https://harumiweb.github.io/exstruct/
|
|
435
|
+
- JSON Schemas: see `schemas/` (one file per model); regenerate via `python scripts/gen_json_schema.py`.
|
|
@@ -12,7 +12,7 @@ ExStruct reads Excel workbooks and outputs structured data (cells, table candida
|
|
|
12
12
|
|
|
13
13
|
- **Excel → Structured JSON**: cells, shapes, charts, table candidates, print areas/views, and auto page-break areas per sheet.
|
|
14
14
|
- **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, print areas), `verbose` (all shapes with width/height, charts with size, print areas). Verbose also emits cell hyperlinks. Size output is flag-controlled.
|
|
15
|
-
- **Auto page-break export (COM only)**: capture Excel-computed auto page breaks and write per-area JSON/YAML/TOON when requested.
|
|
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.
|
|
18
18
|
- **CLI rendering** (Excel required): optional PDF and per-sheet PNGs.
|
|
@@ -44,11 +44,12 @@ exstruct input.xlsx --format yaml # YAML (needs pyyaml)
|
|
|
44
44
|
exstruct input.xlsx --format toon # TOON (needs python-toon)
|
|
45
45
|
exstruct input.xlsx --sheets-dir sheets/ # split per sheet in chosen format
|
|
46
46
|
exstruct input.xlsx --print-areas-dir areas/ # split per print area (if any)
|
|
47
|
+
exstruct input.xlsx --auto-page-breaks-dir auto_areas/ # COM only; option appears when available
|
|
47
48
|
exstruct input.xlsx --mode light # cells + table candidates only
|
|
48
49
|
exstruct input.xlsx --pdf --image # PDF and PNGs (Excel required)
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
Auto page-break exports are API
|
|
52
|
+
Auto page-break exports are available via API and CLI when Excel/COM is available; the CLI exposes `--auto-page-breaks-dir` only in COM-capable environments.
|
|
52
53
|
|
|
53
54
|
## Quick Start (Python)
|
|
54
55
|
|
|
@@ -349,12 +350,24 @@ In short, **exstruct = “an engine that converts Excel into a format AI can und
|
|
|
349
350
|
- Default JSON is compact to reduce tokens; use `--pretty` or `pretty=True` when readability matters.
|
|
350
351
|
- Field `table_candidates` replaces `tables`; adjust downstream consumers accordingly.
|
|
351
352
|
|
|
353
|
+
## Enterprise Use
|
|
354
|
+
|
|
355
|
+
ExStruct is used primarily as a **library**, not a service.
|
|
356
|
+
|
|
357
|
+
- No official support or SLA is provided
|
|
358
|
+
- Long-term stability is prioritized over rapid feature growth
|
|
359
|
+
- Forking and internal modification are expected in enterprise use
|
|
360
|
+
|
|
361
|
+
This project is suitable for teams that:
|
|
362
|
+
- need transparency over black-box tools
|
|
363
|
+
- are comfortable maintaining internal forks if necessary
|
|
364
|
+
|
|
352
365
|
## Print Areas and Auto Page Breaks (PrintArea / PrintAreaView)
|
|
353
366
|
|
|
354
367
|
- `SheetData.print_areas` holds print areas (cell coordinates) in light/standard/verbose.
|
|
355
368
|
- `SheetData.auto_print_areas` holds Excel COM-computed auto page-break areas when auto page-break extraction is enabled (COM only).
|
|
356
369
|
- Use `export_print_areas_as(...)` or CLI `--print-areas-dir` to write one file per print area (nothing is written if none exist).
|
|
357
|
-
- Use `DestinationOptions.auto_page_breaks_dir` (preferred) or `export_auto_page_breaks(...)` to write per-auto-page-break files; the API raises `ValueError` if no auto page breaks exist.
|
|
370
|
+
- Use CLI `--auto-page-breaks-dir` (COM only), `DestinationOptions.auto_page_breaks_dir` (preferred), or `export_auto_page_breaks(...)` to write per-auto-page-break files; the API raises `ValueError` if no auto page breaks exist.
|
|
358
371
|
- `PrintAreaView` includes rows and table candidates inside the area, plus shapes/charts that overlap the area (size-less shapes are treated as points). `normalize=True` rebases row/col indices to the area origin.
|
|
359
372
|
|
|
360
373
|
## License
|
|
@@ -364,3 +377,4 @@ BSD-3-Clause. See `LICENSE` for details.
|
|
|
364
377
|
## Documentation
|
|
365
378
|
|
|
366
379
|
- API Reference (GitHub Pages): https://harumiweb.github.io/exstruct/
|
|
380
|
+
- JSON Schemas: see `schemas/` (one file per model); regenerate via `python scripts/gen_json_schema.py`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
import xlwings as xw
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ComAvailability(BaseModel):
|
|
14
|
+
"""Availability information for Excel COM-dependent features."""
|
|
15
|
+
|
|
16
|
+
available: bool = Field(
|
|
17
|
+
..., description="True when Excel COM can be used from this environment."
|
|
18
|
+
)
|
|
19
|
+
reason: str | None = Field(
|
|
20
|
+
default=None, description="Reason COM features are unavailable."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_com_availability() -> ComAvailability:
|
|
25
|
+
"""Detect whether Excel COM is available for CLI features.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
ComAvailability describing whether COM features can be used.
|
|
29
|
+
"""
|
|
30
|
+
if os.getenv("SKIP_COM_TESTS"):
|
|
31
|
+
return ComAvailability(available=False, reason="SKIP_COM_TESTS is set.")
|
|
32
|
+
|
|
33
|
+
if sys.platform != "win32":
|
|
34
|
+
return ComAvailability(available=False, reason="Non-Windows platform.")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
app = xw.App(add_book=False, visible=False)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
return ComAvailability(
|
|
40
|
+
available=False,
|
|
41
|
+
reason=f"Excel COM is unavailable ({exc.__class__.__name__}).",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
app.quit()
|
|
46
|
+
except Exception:
|
|
47
|
+
logger.warning("Failed to quit Excel during COM availability check.")
|
|
48
|
+
|
|
49
|
+
return ComAvailability(available=True, reason=None)
|
|
@@ -4,9 +4,33 @@ import argparse
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from exstruct import process_excel
|
|
7
|
+
from exstruct.cli.availability import ComAvailability, get_com_availability
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def
|
|
10
|
+
def _add_auto_page_breaks_argument(
|
|
11
|
+
parser: argparse.ArgumentParser, availability: ComAvailability
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Add auto page-break export option when COM is available."""
|
|
14
|
+
if not availability.available:
|
|
15
|
+
return
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--auto-page-breaks-dir",
|
|
18
|
+
type=Path,
|
|
19
|
+
help="Optional directory to write one file per auto page-break area (COM only).",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser(
|
|
24
|
+
availability: ComAvailability | None = None,
|
|
25
|
+
) -> argparse.ArgumentParser:
|
|
26
|
+
"""Build the CLI argument parser.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
availability: Optional COM availability for tests or overrides.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Configured argument parser.
|
|
33
|
+
"""
|
|
10
34
|
parser = argparse.ArgumentParser(
|
|
11
35
|
description="Dev-only CLI stub for ExStruct extraction."
|
|
12
36
|
)
|
|
@@ -62,10 +86,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
62
86
|
type=Path,
|
|
63
87
|
help="Optional directory to write one file per print area (format follows --format).",
|
|
64
88
|
)
|
|
89
|
+
resolved_availability = (
|
|
90
|
+
availability if availability is not None else get_com_availability()
|
|
91
|
+
)
|
|
92
|
+
_add_auto_page_breaks_argument(parser, resolved_availability)
|
|
65
93
|
return parser
|
|
66
94
|
|
|
67
95
|
|
|
68
96
|
def main(argv: list[str] | None = None) -> int:
|
|
97
|
+
"""Run the CLI entrypoint.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
argv: Optional argument list for testing.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Exit code (0 for success, 1 for failure).
|
|
104
|
+
"""
|
|
69
105
|
parser = build_parser()
|
|
70
106
|
args = parser.parse_args(argv)
|
|
71
107
|
|
|
@@ -86,6 +122,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
122
|
pretty=args.pretty,
|
|
87
123
|
sheets_dir=args.sheets_dir,
|
|
88
124
|
print_areas_dir=args.print_areas_dir,
|
|
125
|
+
auto_page_breaks_dir=getattr(args, "auto_page_breaks_dir", None),
|
|
89
126
|
)
|
|
90
127
|
return 0
|
|
91
128
|
except Exception as e:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Iterator
|
|
4
|
-
import math
|
|
5
|
-
from typing import SupportsInt, cast
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
import math
|
|
5
|
+
from typing import SupportsInt, cast
|
|
6
6
|
|
|
7
7
|
import xlwings as xw
|
|
8
8
|
from xlwings import Book
|
|
@@ -23,54 +23,54 @@ def angle_to_compass(angle: float) -> str:
|
|
|
23
23
|
return dirs[idx]
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def coord_to_cell_by_edges(
|
|
27
|
-
row_edges: list[float], col_edges: list[float], x: float, y: float
|
|
28
|
-
) -> str | None:
|
|
29
|
-
"""Estimate cell address from coordinates and cumulative edges; return None if out of range."""
|
|
30
|
-
|
|
31
|
-
def find_index(edges: list[float], pos: float) -> int | None:
|
|
32
|
-
for i in range(1, len(edges)):
|
|
33
|
-
if edges[i - 1] <= pos < edges[i]:
|
|
34
|
-
return i
|
|
35
|
-
return None
|
|
26
|
+
def coord_to_cell_by_edges(
|
|
27
|
+
row_edges: list[float], col_edges: list[float], x: float, y: float
|
|
28
|
+
) -> str | None:
|
|
29
|
+
"""Estimate cell address from coordinates and cumulative edges; return None if out of range."""
|
|
30
|
+
|
|
31
|
+
def find_index(edges: list[float], pos: float) -> int | None:
|
|
32
|
+
for i in range(1, len(edges)):
|
|
33
|
+
if edges[i - 1] <= pos < edges[i]:
|
|
34
|
+
return i
|
|
35
|
+
return None
|
|
36
36
|
|
|
37
37
|
r = find_index(row_edges, y)
|
|
38
38
|
c = find_index(col_edges, x)
|
|
39
39
|
if r is None or c is None:
|
|
40
40
|
return None
|
|
41
|
-
return f"{xw.utils.col_name(c)}{r}"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def has_arrow(style_val: object) -> bool:
|
|
45
|
-
"""Return True if Excel arrow style value indicates an arrowhead."""
|
|
46
|
-
try:
|
|
47
|
-
v = int(cast(SupportsInt, style_val))
|
|
48
|
-
return v != 0
|
|
49
|
-
except Exception:
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def iter_shapes_recursive(shp: xw.Shape) -> Iterator[xw.Shape]:
|
|
54
|
-
"""Yield shapes recursively, including group children."""
|
|
55
|
-
yield shp
|
|
56
|
-
try:
|
|
57
|
-
if shp.api.Type == 6:
|
|
58
|
-
items = shp.api.GroupItems
|
|
59
|
-
for i in range(1, items.Count + 1):
|
|
60
|
-
inner = items.Item(i)
|
|
61
|
-
try:
|
|
62
|
-
name = inner.Name
|
|
63
|
-
xl_shape = shp.parent.shapes[name]
|
|
64
|
-
except Exception:
|
|
65
|
-
xl_shape = None
|
|
66
|
-
|
|
67
|
-
if xl_shape is not None:
|
|
68
|
-
yield from iter_shapes_recursive(xl_shape)
|
|
69
|
-
except Exception:
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _should_include_shape(
|
|
41
|
+
return f"{xw.utils.col_name(c)}{r}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def has_arrow(style_val: object) -> bool:
|
|
45
|
+
"""Return True if Excel arrow style value indicates an arrowhead."""
|
|
46
|
+
try:
|
|
47
|
+
v = int(cast(SupportsInt, style_val))
|
|
48
|
+
return v != 0
|
|
49
|
+
except Exception:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def iter_shapes_recursive(shp: xw.Shape) -> Iterator[xw.Shape]:
|
|
54
|
+
"""Yield shapes recursively, including group children."""
|
|
55
|
+
yield shp
|
|
56
|
+
try:
|
|
57
|
+
if shp.api.Type == 6:
|
|
58
|
+
items = shp.api.GroupItems
|
|
59
|
+
for i in range(1, items.Count + 1):
|
|
60
|
+
inner = items.Item(i)
|
|
61
|
+
try:
|
|
62
|
+
name = inner.Name
|
|
63
|
+
xl_shape = shp.parent.shapes[name]
|
|
64
|
+
except Exception:
|
|
65
|
+
xl_shape = None
|
|
66
|
+
|
|
67
|
+
if xl_shape is not None:
|
|
68
|
+
yield from iter_shapes_recursive(xl_shape)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _should_include_shape(
|
|
74
74
|
*,
|
|
75
75
|
text: str,
|
|
76
76
|
shape_type_num: int | None,
|
|
@@ -108,13 +108,16 @@ def _should_include_shape(
|
|
|
108
108
|
return True
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def get_shapes_with_position( # noqa: C901
|
|
112
|
-
workbook: Book, mode: str = "standard"
|
|
113
|
-
) -> dict[str, list[Shape]]:
|
|
111
|
+
def get_shapes_with_position( # noqa: C901
|
|
112
|
+
workbook: Book, mode: str = "standard"
|
|
113
|
+
) -> dict[str, list[Shape]]:
|
|
114
114
|
"""Scan shapes in a workbook and return per-sheet Shape lists with position info."""
|
|
115
115
|
shape_data: dict[str, list[Shape]] = {}
|
|
116
116
|
for sheet in workbook.sheets:
|
|
117
117
|
shapes: list[Shape] = []
|
|
118
|
+
excel_names: list[tuple[str, int]] = []
|
|
119
|
+
node_index = 0
|
|
120
|
+
pending_connections: list[tuple[Shape, str | None, str | None]] = []
|
|
118
121
|
for root in sheet.shapes:
|
|
119
122
|
for shp in iter_shapes_recursive(root):
|
|
120
123
|
try:
|
|
@@ -168,7 +171,29 @@ def get_shapes_with_position( # noqa: C901
|
|
|
168
171
|
else (shape_type_str or shape_name or "Unknown")
|
|
169
172
|
)
|
|
170
173
|
|
|
174
|
+
is_relationship_geom = False
|
|
175
|
+
if type_num in (3, 9):
|
|
176
|
+
is_relationship_geom = True
|
|
177
|
+
if autoshape_type_str and (
|
|
178
|
+
"Arrow" in autoshape_type_str or "Connector" in autoshape_type_str
|
|
179
|
+
):
|
|
180
|
+
is_relationship_geom = True
|
|
181
|
+
if shape_type_str and (
|
|
182
|
+
"Connector" in shape_type_str or shape_type_str in ("Line", "ConnectLine")
|
|
183
|
+
):
|
|
184
|
+
is_relationship_geom = True
|
|
185
|
+
if shape_name and ("Connector" in shape_name or "Line" in shape_name):
|
|
186
|
+
is_relationship_geom = True
|
|
187
|
+
|
|
188
|
+
shape_id = None
|
|
189
|
+
if not is_relationship_geom:
|
|
190
|
+
node_index += 1
|
|
191
|
+
shape_id = node_index
|
|
192
|
+
|
|
193
|
+
excel_name = shape_name if isinstance(shape_name, str) else None
|
|
194
|
+
|
|
171
195
|
shape_obj = Shape(
|
|
196
|
+
id=shape_id,
|
|
172
197
|
text=text,
|
|
173
198
|
l=int(shp.left),
|
|
174
199
|
t=int(shp.top),
|
|
@@ -180,25 +205,12 @@ def get_shapes_with_position( # noqa: C901
|
|
|
180
205
|
else None,
|
|
181
206
|
type=type_label,
|
|
182
207
|
)
|
|
208
|
+
if excel_name:
|
|
209
|
+
if shape_id is not None:
|
|
210
|
+
excel_names.append((excel_name, shape_id))
|
|
183
211
|
try:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
is_relationship_geom = True
|
|
187
|
-
if autoshape_type_str and (
|
|
188
|
-
"Arrow" in autoshape_type_str
|
|
189
|
-
or "Connector" in autoshape_type_str
|
|
190
|
-
):
|
|
191
|
-
is_relationship_geom = True
|
|
192
|
-
if shape_type_str and (
|
|
193
|
-
"Connector" in shape_type_str
|
|
194
|
-
or shape_type_str in ("Line", "ConnectLine")
|
|
195
|
-
):
|
|
196
|
-
is_relationship_geom = True
|
|
197
|
-
if shape_name and (
|
|
198
|
-
"Connector" in shape_name or "Line" in shape_name
|
|
199
|
-
):
|
|
200
|
-
is_relationship_geom = True
|
|
201
|
-
|
|
212
|
+
begin_name: str | None = None
|
|
213
|
+
end_name: str | None = None
|
|
202
214
|
if is_relationship_geom:
|
|
203
215
|
angle = compute_line_angle_deg(
|
|
204
216
|
float(shp.width), float(shp.height)
|
|
@@ -217,6 +229,28 @@ def get_shapes_with_position( # noqa: C901
|
|
|
217
229
|
shape_obj.end_arrow_style = end_style
|
|
218
230
|
except Exception:
|
|
219
231
|
pass
|
|
232
|
+
# Connector begin/end connected shapes (if this shape is a connector).
|
|
233
|
+
try:
|
|
234
|
+
connector = shp.api.ConnectorFormat
|
|
235
|
+
try:
|
|
236
|
+
begin_shape = connector.BeginConnectedShape
|
|
237
|
+
if begin_shape is not None:
|
|
238
|
+
name = getattr(begin_shape, "Name", None)
|
|
239
|
+
if isinstance(name, str):
|
|
240
|
+
begin_name = name
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
try:
|
|
244
|
+
end_shape = connector.EndConnectedShape
|
|
245
|
+
if end_shape is not None:
|
|
246
|
+
name = getattr(end_shape, "Name", None)
|
|
247
|
+
if isinstance(name, str):
|
|
248
|
+
end_name = name
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
except Exception:
|
|
252
|
+
# Not a connector or ConnectorFormat is unavailable.
|
|
253
|
+
pass
|
|
220
254
|
elif type_num == 1 and (
|
|
221
255
|
autoshape_type_str and "Arrow" in autoshape_type_str
|
|
222
256
|
):
|
|
@@ -228,6 +262,14 @@ def get_shapes_with_position( # noqa: C901
|
|
|
228
262
|
pass
|
|
229
263
|
except Exception:
|
|
230
264
|
pass
|
|
265
|
+
pending_connections.append((shape_obj, begin_name, end_name))
|
|
231
266
|
shapes.append(shape_obj)
|
|
267
|
+
if pending_connections:
|
|
268
|
+
name_to_id = {name: sid for name, sid in excel_names}
|
|
269
|
+
for shape_obj, begin_name, end_name in pending_connections:
|
|
270
|
+
if begin_name:
|
|
271
|
+
shape_obj.begin_id = name_to_id.get(begin_name)
|
|
272
|
+
if end_name:
|
|
273
|
+
shape_obj.end_id = name_to_id.get(end_name)
|
|
232
274
|
shape_data[sheet.name] = shapes
|
|
233
275
|
return shape_data
|
|
@@ -11,6 +11,9 @@ from pydantic import BaseModel, Field
|
|
|
11
11
|
class Shape(BaseModel):
|
|
12
12
|
"""Shape metadata (position, size, text, and styling)."""
|
|
13
13
|
|
|
14
|
+
id: int | None = Field(
|
|
15
|
+
default=None, description="Sequential shape id within the sheet (if applicable)."
|
|
16
|
+
)
|
|
14
17
|
text: str = Field(description="Visible text content of the shape.")
|
|
15
18
|
l: int = Field(description="Left offset (Excel units).") # noqa: E741
|
|
16
19
|
t: int = Field(description="Top offset (Excel units).")
|
|
@@ -26,6 +29,18 @@ class Shape(BaseModel):
|
|
|
26
29
|
end_arrow_style: int | None = Field(
|
|
27
30
|
default=None, description="Arrow style enum for the end of a connector."
|
|
28
31
|
)
|
|
32
|
+
begin_id: int | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description=(
|
|
35
|
+
"Shape id at the start of a connector (ConnectorFormat.BeginConnectedShape)."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
end_id: int | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description=(
|
|
41
|
+
"Shape id at the end of a connector (ConnectorFormat.EndConnectedShape)."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
29
44
|
direction: Literal["E", "SE", "S", "SW", "W", "NW", "N", "NE"] | None = Field(
|
|
30
45
|
default=None, description="Connector direction (compass heading)."
|
|
31
46
|
)
|
|
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
|