exstruct 0.1.0__py3-none-any.whl
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/__init__.py +116 -0
- exstruct/cli/main.py +93 -0
- exstruct/core/__init__.py +0 -0
- exstruct/core/cells.py +950 -0
- exstruct/core/charts.py +229 -0
- exstruct/core/integrate.py +132 -0
- exstruct/core/shapes.py +212 -0
- exstruct/io/__init__.py +179 -0
- exstruct/models/__init__.py +51 -0
- exstruct/models/maps.py +335 -0
- exstruct/py.typed +0 -0
- exstruct/render/__init__.py +89 -0
- exstruct-0.1.0.dist-info/METADATA +154 -0
- exstruct-0.1.0.dist-info/RECORD +16 -0
- exstruct-0.1.0.dist-info/WHEEL +4 -0
- exstruct-0.1.0.dist-info/entry_points.txt +3 -0
exstruct/io/__init__.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Literal
|
|
7
|
+
|
|
8
|
+
from ..models import WorkbookData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def dict_without_empty_values(obj: Any):
|
|
12
|
+
"""Recursively drop empty values from nested structures."""
|
|
13
|
+
if isinstance(obj, dict):
|
|
14
|
+
return {
|
|
15
|
+
k: dict_without_empty_values(v)
|
|
16
|
+
for k, v in obj.items()
|
|
17
|
+
if v not in [None, "", [], {}]
|
|
18
|
+
}
|
|
19
|
+
if isinstance(obj, list):
|
|
20
|
+
return [
|
|
21
|
+
dict_without_empty_values(v) for v in obj if v not in [None, "", [], {}]
|
|
22
|
+
]
|
|
23
|
+
if hasattr(obj, "model_dump"):
|
|
24
|
+
return dict_without_empty_values(obj.model_dump(exclude_none=True))
|
|
25
|
+
return obj
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_as_json(model: WorkbookData, path: Path, *, pretty: bool = False, indent: int | None = None) -> None:
|
|
29
|
+
filtered_dict = dict_without_empty_values(model)
|
|
30
|
+
indent_val = 2 if pretty and indent is None else indent
|
|
31
|
+
path.write_text(
|
|
32
|
+
json.dumps(filtered_dict, ensure_ascii=False, indent=indent_val),
|
|
33
|
+
encoding="utf-8",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save_as_yaml(model: WorkbookData, path: Path) -> None:
|
|
38
|
+
try:
|
|
39
|
+
import yaml
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"YAML export requires pyyaml. Install it via `pip install pyyaml` "
|
|
43
|
+
"or add the 'yaml' extra."
|
|
44
|
+
) from e
|
|
45
|
+
|
|
46
|
+
filtered_dict = dict_without_empty_values(model)
|
|
47
|
+
text = yaml.safe_dump(
|
|
48
|
+
filtered_dict,
|
|
49
|
+
allow_unicode=True,
|
|
50
|
+
sort_keys=False,
|
|
51
|
+
indent=2,
|
|
52
|
+
)
|
|
53
|
+
path.write_text(text, encoding="utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_as_toon(model: WorkbookData, path: Path) -> None:
|
|
57
|
+
try:
|
|
58
|
+
import toon
|
|
59
|
+
except ImportError as e:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"TOON export requires python-toon. Install it via `pip install python-toon` "
|
|
62
|
+
"or add the 'toon' extra."
|
|
63
|
+
) from e
|
|
64
|
+
|
|
65
|
+
filtered_dict = dict_without_empty_values(model)
|
|
66
|
+
text = toon.encode(filtered_dict)
|
|
67
|
+
path.write_text(text, encoding="utf-8")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sanitize_sheet_filename(name: str) -> str:
|
|
71
|
+
"""Make a sheet name safe for filesystem usage."""
|
|
72
|
+
safe = re.sub(r"[\\/:*?\"<>|]", "_", name)
|
|
73
|
+
return safe or "sheet"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def save_sheets_as_json(workbook: WorkbookData, output_dir: Path, *, pretty: bool = False, indent: int | None = None) -> Dict[str, Path]:
|
|
77
|
+
"""
|
|
78
|
+
Save each sheet as an individual JSON file.
|
|
79
|
+
Contents include book_name and the sheet's SheetData.
|
|
80
|
+
Returns a map of sheet name -> written path.
|
|
81
|
+
"""
|
|
82
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
written: Dict[str, Path] = {}
|
|
84
|
+
for sheet_name, sheet_data in workbook.sheets.items():
|
|
85
|
+
payload = dict_without_empty_values(
|
|
86
|
+
{
|
|
87
|
+
"book_name": workbook.book_name,
|
|
88
|
+
"sheet_name": sheet_name,
|
|
89
|
+
"sheet": sheet_data,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
file_name = f"{_sanitize_sheet_filename(sheet_name)}.json"
|
|
93
|
+
path = output_dir / file_name
|
|
94
|
+
indent_val = 2 if pretty and indent is None else indent
|
|
95
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=indent_val), encoding="utf-8")
|
|
96
|
+
written[sheet_name] = path
|
|
97
|
+
return written
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def save_sheets(
|
|
101
|
+
workbook: WorkbookData,
|
|
102
|
+
output_dir: Path,
|
|
103
|
+
fmt: Literal["json", "yaml", "yml", "toon"] = "json",
|
|
104
|
+
*,
|
|
105
|
+
pretty: bool = False,
|
|
106
|
+
indent: int | None = None,
|
|
107
|
+
) -> Dict[str, Path]:
|
|
108
|
+
"""
|
|
109
|
+
Save each sheet as an individual file in the specified format (json/yaml/toon).
|
|
110
|
+
Payload includes book_name and the sheet's SheetData.
|
|
111
|
+
"""
|
|
112
|
+
format_hint = fmt.lower()
|
|
113
|
+
if format_hint == "yml":
|
|
114
|
+
format_hint = "yaml"
|
|
115
|
+
if format_hint not in ("json", "yaml", "toon"):
|
|
116
|
+
raise ValueError(f"Unsupported sheet export format: {fmt}")
|
|
117
|
+
|
|
118
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
written: Dict[str, Path] = {}
|
|
120
|
+
for sheet_name, sheet_data in workbook.sheets.items():
|
|
121
|
+
payload = dict_without_empty_values(
|
|
122
|
+
{
|
|
123
|
+
"book_name": workbook.book_name,
|
|
124
|
+
"sheet_name": sheet_name,
|
|
125
|
+
"sheet": sheet_data,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
suffix = {"json": ".json", "yaml": ".yaml", "toon": ".toon"}[format_hint]
|
|
129
|
+
file_name = f"{_sanitize_sheet_filename(sheet_name)}{suffix}"
|
|
130
|
+
path = output_dir / file_name
|
|
131
|
+
match format_hint:
|
|
132
|
+
case "json":
|
|
133
|
+
indent_val = 2 if pretty and indent is None else indent
|
|
134
|
+
text = json.dumps(payload, ensure_ascii=False, indent=indent_val)
|
|
135
|
+
case "yaml":
|
|
136
|
+
yaml = _require_yaml()
|
|
137
|
+
text = yaml.safe_dump(
|
|
138
|
+
payload, allow_unicode=True, sort_keys=False, indent=2
|
|
139
|
+
)
|
|
140
|
+
case "toon":
|
|
141
|
+
toon = _require_toon()
|
|
142
|
+
text = toon.encode(payload)
|
|
143
|
+
case _:
|
|
144
|
+
raise ValueError(f"Unsupported sheet export format: {format_hint}")
|
|
145
|
+
path.write_text(text, encoding="utf-8")
|
|
146
|
+
written[sheet_name] = path
|
|
147
|
+
return written
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _require_yaml():
|
|
151
|
+
try:
|
|
152
|
+
import yaml
|
|
153
|
+
except ImportError as e:
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
"YAML export requires pyyaml. Install it via `pip install pyyaml` "
|
|
156
|
+
"or add the 'yaml' extra."
|
|
157
|
+
) from e
|
|
158
|
+
return yaml
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _require_toon():
|
|
162
|
+
try:
|
|
163
|
+
import toon
|
|
164
|
+
except ImportError as e:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
"TOON export requires python-toon. Install it via `pip install python-toon` "
|
|
167
|
+
"or add the 'toon' extra."
|
|
168
|
+
) from e
|
|
169
|
+
return toon
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = [
|
|
173
|
+
"dict_without_empty_values",
|
|
174
|
+
"save_as_json",
|
|
175
|
+
"save_as_yaml",
|
|
176
|
+
"save_as_toon",
|
|
177
|
+
"save_sheets",
|
|
178
|
+
"save_sheets_as_json",
|
|
179
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import List, Dict, Optional, Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Shape(BaseModel):
|
|
6
|
+
text: str
|
|
7
|
+
l: int
|
|
8
|
+
t: int
|
|
9
|
+
w: Optional[int]
|
|
10
|
+
h: Optional[int]
|
|
11
|
+
type: Optional[str] = None
|
|
12
|
+
rotation: Optional[float] = None
|
|
13
|
+
begin_arrow_style: Optional[int] = None
|
|
14
|
+
end_arrow_style: Optional[int] = None
|
|
15
|
+
direction: Optional[Literal["E", "SE", "S", "SW", "W", "NW", "N", "NE"]] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CellRow(BaseModel):
|
|
19
|
+
r: int
|
|
20
|
+
c: Dict[str, int | float | str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChartSeries(BaseModel):
|
|
24
|
+
name: str
|
|
25
|
+
name_range: Optional[str] = None
|
|
26
|
+
x_range: Optional[str] = None
|
|
27
|
+
y_range: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Chart(BaseModel):
|
|
31
|
+
name: str
|
|
32
|
+
chart_type: str
|
|
33
|
+
title: Optional[str]
|
|
34
|
+
y_axis_title: str
|
|
35
|
+
y_axis_range: List[float] = Field(default_factory=list)
|
|
36
|
+
series: List[ChartSeries]
|
|
37
|
+
l: int
|
|
38
|
+
t: int
|
|
39
|
+
error: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SheetData(BaseModel):
|
|
43
|
+
rows: List[CellRow] = Field(default_factory=list)
|
|
44
|
+
shapes: List[Shape] = Field(default_factory=list)
|
|
45
|
+
charts: List[Chart] = Field(default_factory=list)
|
|
46
|
+
table_candidates: List[str] = Field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WorkbookData(BaseModel):
|
|
50
|
+
book_name: str
|
|
51
|
+
sheets: Dict[str, SheetData]
|
exstruct/models/maps.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# msoShapeType
|
|
2
|
+
MSO_SHAPE_TYPE_MAP = {
|
|
3
|
+
30: "3DModel", # 3D モデル
|
|
4
|
+
1: "AutoShape", # オートシェイプ
|
|
5
|
+
2: "Callout", # 吹き出し
|
|
6
|
+
20: "Canvas", # キャンバス
|
|
7
|
+
3: "Chart", # グラフ
|
|
8
|
+
4: "Comment", # コメント
|
|
9
|
+
27: "ContentApp", # コンテンツ Office アドイン
|
|
10
|
+
21: "Diagram", # ダイアグラム
|
|
11
|
+
7: "EmbeddedOLEObject", # 埋め込み OLE オブジェクト
|
|
12
|
+
8: "FormControl", # フォーム コントロール
|
|
13
|
+
5: "Freeform", # フリーフォーム
|
|
14
|
+
28: "Graphic", # グラフィック
|
|
15
|
+
6: "Group", # グループ
|
|
16
|
+
24: "IgxGraphic", # SmartArt グラフィック
|
|
17
|
+
22: "Ink", # インク
|
|
18
|
+
23: "InkComment", # インク コメント
|
|
19
|
+
9: "Line", # Line
|
|
20
|
+
31: "Linked3DModel", # リンクされた 3D モデル
|
|
21
|
+
29: "LinkedGraphic", # リンクされたグラフィック
|
|
22
|
+
10: "LinkedOLEObject", # リンク OLE オブジェクト
|
|
23
|
+
11: "LinkedPicture", # リンク画像
|
|
24
|
+
16: "Media", # メディア
|
|
25
|
+
12: "OLEControlObject", # OLE コントロール オブジェクト
|
|
26
|
+
13: "Picture", # 画像
|
|
27
|
+
14: "Placeholder", # プレースホルダー
|
|
28
|
+
18: "ScriptAnchor", # スクリプト アンカー
|
|
29
|
+
-2: "ShapeTypeMixed", # 図形の種類の組み合わせ
|
|
30
|
+
25: "Slicer", # Slicer
|
|
31
|
+
19: "Table", # テーブル
|
|
32
|
+
17: "TextBox", # テキスト ボックス
|
|
33
|
+
15: "TextEffect", # テキスト効果
|
|
34
|
+
26: "WebVideo", # Web ビデオ
|
|
35
|
+
}
|
|
36
|
+
# AutoShapeType(Type==1のときのみ)
|
|
37
|
+
MSO_AUTO_SHAPE_TYPE_MAP = {
|
|
38
|
+
149: "10pointStar", # 10-point star
|
|
39
|
+
150: "12pointStar", # 12-point star
|
|
40
|
+
94: "16pointStar", # 16-point star
|
|
41
|
+
95: "24pointStar", # 24-point star
|
|
42
|
+
96: "32pointStar", # 32-point star
|
|
43
|
+
91: "4pointStar", # 4-point star
|
|
44
|
+
92: "5pointStar", # 5-point star
|
|
45
|
+
147: "6pointStar", # 6-point star
|
|
46
|
+
148: "7pointStar", # 7-point star
|
|
47
|
+
93: "8pointStar", # 8-point star
|
|
48
|
+
129: "ActionButtonBackorPrevious", # Back or Previous button
|
|
49
|
+
131: "ActionButtonBeginning", # Beginning button
|
|
50
|
+
125: "ActionButtonCustom", # Custom action button
|
|
51
|
+
134: "ActionButtonDocument", # Document button
|
|
52
|
+
132: "ActionButtonEnd", # End button
|
|
53
|
+
130: "ActionButtonForwardorNext", # Forward or Next button
|
|
54
|
+
127: "ActionButtonHelp", # Help button
|
|
55
|
+
126: "ActionButtonHome", # Home button
|
|
56
|
+
128: "ActionButtonInformation", # Information button
|
|
57
|
+
136: "ActionButtonMovie", # Movie button
|
|
58
|
+
133: "ActionButtonReturn", # Return button
|
|
59
|
+
135: "ActionButtonSound", # Sound button
|
|
60
|
+
25: "Arc", # Arc
|
|
61
|
+
137: "Balloon", # Balloon
|
|
62
|
+
41: "BentArrow", # Bent Arrow
|
|
63
|
+
44: "BentUpArrow", # Bent Up Arrow
|
|
64
|
+
15: "Bevel", # Bevel
|
|
65
|
+
20: "BlockArc", # Block Arc
|
|
66
|
+
13: "Can", # Can
|
|
67
|
+
182: "ChartPlus", # Chart Plus
|
|
68
|
+
181: "ChartStar", # Chart Star
|
|
69
|
+
180: "ChartX", # Chart X
|
|
70
|
+
52: "Chevron", # Chevron
|
|
71
|
+
161: "Chord", # Chord
|
|
72
|
+
60: "CircularArrow", # Circular Arrow
|
|
73
|
+
179: "Cloud", # Cloud
|
|
74
|
+
108: "CloudCallout", # Cloud Callout
|
|
75
|
+
162: "Corner", # Corner
|
|
76
|
+
169: "CornerTabs", # Corner Tabs
|
|
77
|
+
11: "Cross", # Cross
|
|
78
|
+
14: "Cube", # Cube
|
|
79
|
+
48: "CurvedDownArrow", # Curved Down Arrow
|
|
80
|
+
100: "CurvedDownRibbon", # Curved Down Ribbon
|
|
81
|
+
46: "CurvedLeftArrow", # Curved Left Arrow
|
|
82
|
+
45: "CurvedRightArrow", # Curved Right Arrow
|
|
83
|
+
47: "CurvedUpArrow", # Curved Up Arrow
|
|
84
|
+
99: "CurvedUpRibbon", # Curved Up Ribbon
|
|
85
|
+
144: "Decagon", # Decagon
|
|
86
|
+
141: "DiagonalStripe", # Diagonal Stripe
|
|
87
|
+
4: "Diamond", # Diamond
|
|
88
|
+
146: "Dodecagon", # Dodecagon
|
|
89
|
+
18: "Donut", # Donut
|
|
90
|
+
27: "DoubleBrace", # Double Brace
|
|
91
|
+
26: "DoubleBracket", # Double Bracket
|
|
92
|
+
104: "DoubleWave", # Double Wave
|
|
93
|
+
36: "DownArrow", # Down Arrow
|
|
94
|
+
56: "DownArrowCallout", # Down Arrow Callout
|
|
95
|
+
98: "DownRibbon", # Down Ribbon
|
|
96
|
+
89: "Explosion1", # Explosion 1
|
|
97
|
+
90: "Explosion2", # Explosion 2
|
|
98
|
+
62: "FlowchartAlternateProcess", # Alternate Process
|
|
99
|
+
75: "FlowchartCard", # Card
|
|
100
|
+
79: "FlowchartCollate", # Collate
|
|
101
|
+
73: "FlowchartConnector", # Connector
|
|
102
|
+
64: "FlowchartData", # Data
|
|
103
|
+
63: "FlowchartDecision", # Decision
|
|
104
|
+
84: "FlowchartDelay", # Delay
|
|
105
|
+
87: "FlowchartDirectAccessStorage", # Direct Access Storage
|
|
106
|
+
88: "FlowchartDisplay", # Display
|
|
107
|
+
67: "FlowchartDocument", # Document
|
|
108
|
+
81: "FlowchartExtract", # Extract
|
|
109
|
+
66: "FlowchartInternalStorage", # Internal Storage
|
|
110
|
+
86: "FlowchartMagneticDisk", # Magnetic Disk
|
|
111
|
+
71: "FlowchartManualInput", # Manual Input
|
|
112
|
+
72: "FlowchartManualOperation", # Manual Operation
|
|
113
|
+
82: "FlowchartMerge", # Merge
|
|
114
|
+
68: "FlowchartMultidocument", # Multidocument
|
|
115
|
+
139: "FlowchartOfflineStorage", # Offline Storage
|
|
116
|
+
74: "FlowchartOffpageConnector", # Offpage Connector
|
|
117
|
+
78: "FlowchartOr", # Or
|
|
118
|
+
65: "FlowchartPredefinedProcess", # Predefined Process
|
|
119
|
+
70: "FlowchartPreparation", # Preparation
|
|
120
|
+
61: "FlowchartProcess", # Process
|
|
121
|
+
76: "FlowchartPunchedTape", # Punched Tape
|
|
122
|
+
85: "FlowchartSequentialAccessStorage", # Sequential Access Storage
|
|
123
|
+
80: "FlowchartSort", # Sort
|
|
124
|
+
83: "FlowchartStoredData", # Stored Data
|
|
125
|
+
77: "FlowchartSummingJunction", # Summing Junction
|
|
126
|
+
69: "FlowchartTerminator", # Terminator
|
|
127
|
+
16: "FoldedCorner", # Folded Corner
|
|
128
|
+
158: "Frame", # Frame
|
|
129
|
+
174: "Funnel", # Funnel
|
|
130
|
+
172: "Gear6", # Gear with six teeth
|
|
131
|
+
173: "Gear9", # Gear with nine teeth
|
|
132
|
+
159: "HalfFrame", # Half Frame
|
|
133
|
+
21: "Heart", # Heart
|
|
134
|
+
145: "Heptagon", # Heptagon
|
|
135
|
+
10: "Hexagon", # Hexagon
|
|
136
|
+
102: "HorizontalScroll", # Horizontal Scroll
|
|
137
|
+
7: "IsoscelesTriangle", # Isosceles Triangle
|
|
138
|
+
34: "LeftArrow", # Left Arrow
|
|
139
|
+
54: "LeftArrowCallout", # Left Arrow Callout
|
|
140
|
+
31: "LeftBrace", # Left Brace
|
|
141
|
+
29: "LeftBracket", # Left Bracket
|
|
142
|
+
176: "LeftCircularArrow", # Left Circular Arrow
|
|
143
|
+
37: "LeftRightArrow", # LeftRight Arrow
|
|
144
|
+
57: "LeftRightArrowCallout", # LeftRight Arrow Callout
|
|
145
|
+
177: "LeftRightCircularArrow", # LeftRight Circular Arrow
|
|
146
|
+
140: "LeftRightRibbon", # LeftRight Ribbon
|
|
147
|
+
40: "LeftRightUpArrow", # LeftRightUp Arrow
|
|
148
|
+
43: "LeftUpArrow", # LeftUp Arrow
|
|
149
|
+
22: "LightningBolt", # Lightning Bolt
|
|
150
|
+
109: "LineCallout1", # Line Callout 1
|
|
151
|
+
113: "LineCallout1AccentBar", # Line Callout 1 Accent Bar
|
|
152
|
+
121: "LineCallout1BorderandAccentBar", # Line Callout 1 Border and Accent Bar
|
|
153
|
+
117: "LineCallout1NoBorder", # Line Callout 1 No Border
|
|
154
|
+
110: "LineCallout2", # Line Callout 2
|
|
155
|
+
114: "LineCallout2AccentBar", # Line Callout 2 Accent Bar
|
|
156
|
+
122: "LineCallout2BorderandAccentBar", # Line Callout 2 Border and Accent Bar
|
|
157
|
+
118: "LineCallout2NoBorder", # Line Callout 2 No Border
|
|
158
|
+
111: "LineCallout3", # Line Callout 3
|
|
159
|
+
115: "LineCallout3AccentBar", # Line Callout 3 Accent Bar
|
|
160
|
+
123: "LineCallout3BorderandAccentBar", # Line Callout 3 Border and Accent Bar
|
|
161
|
+
119: "LineCallout3NoBorder", # Line Callout 3 No Border
|
|
162
|
+
112: "LineCallout4", # Line Callout 4
|
|
163
|
+
116: "LineCallout4AccentBar", # Line Callout 4 Accent Bar
|
|
164
|
+
124: "LineCallout4BorderandAccentBar", # Line Callout 4 Border and Accent Bar
|
|
165
|
+
120: "LineCallout4NoBorder", # Line Callout 4 No Border
|
|
166
|
+
183: "LineInverse", # Line inverse
|
|
167
|
+
166: "MathDivide", # Math Divide
|
|
168
|
+
167: "MathEqual", # Math Equal
|
|
169
|
+
164: "MathMinus", # Math Minus
|
|
170
|
+
165: "MathMultiply", # Math Multiply
|
|
171
|
+
168: "MathNotEqual", # Math Not Equal
|
|
172
|
+
163: "MathPlus", # Math Plus
|
|
173
|
+
-2: "Mixed", # Mixed (combination)
|
|
174
|
+
24: "Moon", # Moon
|
|
175
|
+
143: "NonIsoscelesTrapezoid", # Non Isosceles Trapezoid
|
|
176
|
+
19: "NoSymbol", # No symbol
|
|
177
|
+
50: "NotchedRightArrow", # Notched Right Arrow
|
|
178
|
+
138: "NotPrimitive", # Not supported
|
|
179
|
+
6: "Octagon", # Octagon
|
|
180
|
+
9: "Oval", # Oval
|
|
181
|
+
107: "OvalCallout", # Oval Callout
|
|
182
|
+
2: "Parallelogram", # Parallelogram
|
|
183
|
+
51: "Pentagon", # Pentagon
|
|
184
|
+
142: "Pie", # Pie
|
|
185
|
+
175: "PieWedge", # Pie Wedge
|
|
186
|
+
28: "Plaque", # Plaque
|
|
187
|
+
171: "PlaqueTabs", # Plaque Tabs
|
|
188
|
+
39: "QuadArrow", # Quad Arrow
|
|
189
|
+
59: "QuadArrowCallout", # Quad Arrow Callout
|
|
190
|
+
1: "Rectangle", # Rectangle
|
|
191
|
+
105: "RectangularCallout", # Rectangular Callout
|
|
192
|
+
12: "RegularPentagon", # Regular Pentagon
|
|
193
|
+
33: "RightArrow", # Right Arrow
|
|
194
|
+
53: "RightArrowCallout", # Right Arrow Callout
|
|
195
|
+
32: "RightBrace", # Right Brace
|
|
196
|
+
30: "RightBracket", # Right Bracket
|
|
197
|
+
8: "RightTriangle", # Right Triangle
|
|
198
|
+
151: "Round1Rectangle", # Round 1 Rectangle
|
|
199
|
+
157: "Round2DiagRectangle", # Round 2 Diagonal Rectangle
|
|
200
|
+
152: "Round2SameRectangle", # Round 2 Same Rectangle
|
|
201
|
+
5: "RoundedRectangle", # Rounded Rectangle
|
|
202
|
+
106: "RoundedRectangularCallout", # Rounded Rectangular Callout
|
|
203
|
+
17: "SmileyFace", # Smiley Face
|
|
204
|
+
155: "Snip1Rectangle", # Snip 1 Rectangle
|
|
205
|
+
157: "Snip2DiagRectangle", # Snip 2 Diagonal Rectangle
|
|
206
|
+
156: "Snip2SameRectangle", # Snip 2 Same Rectangle
|
|
207
|
+
154: "SnipRoundRectangle", # Snip Round Rectangle
|
|
208
|
+
170: "SquareTabs", # Square Tabs
|
|
209
|
+
49: "StripedRightArrow", # Striped Right Arrow
|
|
210
|
+
23: "Sun", # Sun
|
|
211
|
+
178: "SwooshArrow", # Swoosh Arrow
|
|
212
|
+
160: "Tear", # Tear
|
|
213
|
+
3: "Trapezoid", # Trapezoid
|
|
214
|
+
35: "UpArrow", # Up Arrow
|
|
215
|
+
55: "UpArrowCallout", # Up Arrow Callout
|
|
216
|
+
38: "UpDownArrow", # Up Down Arrow
|
|
217
|
+
58: "UpDownArrowCallout", # Up Down Arrow Callout
|
|
218
|
+
97: "UpRibbon", # Up Ribbon
|
|
219
|
+
42: "UTurnArrow", # U-Turn Arrow
|
|
220
|
+
101: "VerticalScroll", # Vertical Scroll
|
|
221
|
+
103: "Wave", # Wave
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
XL_CHART_TYPE_MAP = {
|
|
225
|
+
-4098: "3DArea",
|
|
226
|
+
78: "3DAreaStacked",
|
|
227
|
+
79: "3DAreaStacked100",
|
|
228
|
+
60: "3DBarClustered",
|
|
229
|
+
61: "3DBarStacked",
|
|
230
|
+
62: "3DBarStacked100",
|
|
231
|
+
-4100: "3DColumn",
|
|
232
|
+
54: "3DColumnClustered",
|
|
233
|
+
55: "3DColumnStacked",
|
|
234
|
+
56: "3DColumnStacked100",
|
|
235
|
+
-4101: "3DLine",
|
|
236
|
+
-4102: "3DPie",
|
|
237
|
+
70: "3DPieExploded",
|
|
238
|
+
1: "Area",
|
|
239
|
+
135: "AreaEx",
|
|
240
|
+
76: "AreaStacked",
|
|
241
|
+
77: "AreaStacked100",
|
|
242
|
+
137: "AreaStacked100Ex",
|
|
243
|
+
136: "AreaStackedEx",
|
|
244
|
+
57: "BarClustered",
|
|
245
|
+
132: "BarClusteredEx",
|
|
246
|
+
71: "BarOfPie",
|
|
247
|
+
58: "BarStacked",
|
|
248
|
+
59: "BarStacked100",
|
|
249
|
+
134: "BarStacked100Ex",
|
|
250
|
+
133: "BarStackedEx",
|
|
251
|
+
121: "Boxwhisker",
|
|
252
|
+
15: "Bubble",
|
|
253
|
+
87: "Bubble3DEffect",
|
|
254
|
+
139: "BubbleEx",
|
|
255
|
+
51: "ColumnClustered",
|
|
256
|
+
124: "ColumnClusteredEx",
|
|
257
|
+
52: "ColumnStacked",
|
|
258
|
+
53: "ColumnStacked100",
|
|
259
|
+
126: "ColumnStacked100Ex",
|
|
260
|
+
125: "ColumnStackedEx",
|
|
261
|
+
-4152: "Combo",
|
|
262
|
+
115: "ComboAreaStackedColumnClustered",
|
|
263
|
+
113: "ComboColumnClusteredLine",
|
|
264
|
+
114: "ComboColumnClusteredLineSecondaryAxis",
|
|
265
|
+
102: "ConeBarClustered",
|
|
266
|
+
103: "ConeBarStacked",
|
|
267
|
+
104: "ConeBarStacked100",
|
|
268
|
+
105: "ConeCol",
|
|
269
|
+
99: "ConeColClustered",
|
|
270
|
+
100: "ConeColStacked",
|
|
271
|
+
101: "ConeColStacked100",
|
|
272
|
+
95: "CylinderBarClustered",
|
|
273
|
+
96: "CylinderBarStacked",
|
|
274
|
+
97: "CylinderBarStacked100",
|
|
275
|
+
98: "CylinderCol",
|
|
276
|
+
92: "CylinderColClustered",
|
|
277
|
+
93: "CylinderColStacked",
|
|
278
|
+
94: "CylinderColStacked100",
|
|
279
|
+
-4120: "Doughnut",
|
|
280
|
+
131: "DoughnutEx",
|
|
281
|
+
80: "DoughnutExploded",
|
|
282
|
+
123: "Funnel",
|
|
283
|
+
118: "Histogram",
|
|
284
|
+
4: "Line",
|
|
285
|
+
127: "LineEx",
|
|
286
|
+
65: "LineMarkers",
|
|
287
|
+
66: "LineMarkersStacked",
|
|
288
|
+
67: "LineMarkersStacked100",
|
|
289
|
+
63: "LineStacked",
|
|
290
|
+
64: "LineStacked100",
|
|
291
|
+
129: "LineStacked100Ex",
|
|
292
|
+
128: "LineStackedEx",
|
|
293
|
+
116: "OtherCombinations",
|
|
294
|
+
122: "Pareto",
|
|
295
|
+
5: "Pie",
|
|
296
|
+
130: "PieEx",
|
|
297
|
+
69: "PieExploded",
|
|
298
|
+
68: "PieOfPie",
|
|
299
|
+
109: "PyramidBarClustered",
|
|
300
|
+
110: "PyramidBarStacked",
|
|
301
|
+
111: "PyramidBarStacked100",
|
|
302
|
+
112: "PyramidCol",
|
|
303
|
+
106: "PyramidColClustered",
|
|
304
|
+
107: "PyramidColStacked",
|
|
305
|
+
108: "PyramidColStacked100",
|
|
306
|
+
-4151: "Radar",
|
|
307
|
+
82: "RadarFilled",
|
|
308
|
+
81: "RadarMarkers",
|
|
309
|
+
140: "RegionMap",
|
|
310
|
+
88: "StockHLC",
|
|
311
|
+
89: "StockOHLC",
|
|
312
|
+
90: "StockVHLC",
|
|
313
|
+
91: "StockVOHLC",
|
|
314
|
+
-2: "SuggestedChart",
|
|
315
|
+
120: "Sunburst",
|
|
316
|
+
83: "Surface",
|
|
317
|
+
85: "SurfaceTopView",
|
|
318
|
+
86: "SurfaceTopViewWireframe",
|
|
319
|
+
84: "SurfaceWireframe",
|
|
320
|
+
117: "Treemap",
|
|
321
|
+
119: "Waterfall",
|
|
322
|
+
-4169: "XYScatter",
|
|
323
|
+
138: "XYScatterEx",
|
|
324
|
+
74: "XYScatterLines",
|
|
325
|
+
75: "XYScatterLinesNoMarkers",
|
|
326
|
+
72: "XYScatterSmooth",
|
|
327
|
+
73: "XYScatterSmoothNoMarkers",
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
XL_CHART_TYPE_PIE = {5, 18, 69, 68, 130} # Pie, PieExploded, PieOfPie, PieEx
|
|
331
|
+
XL_CHART_TYPE_DOUGHNUT = {-4120, 131, 80} # Doughnut, DoughnutEx, DoughnutExploded
|
|
332
|
+
XL_CHART_TYPE_XY_SCATTER = {-4169, 72, 74, 75, 73, 138} # XYScatter variants
|
|
333
|
+
XL_CHART_TYPE_BOXWHISKER = {121}
|
|
334
|
+
XL_CHART_TYPE_BUBBLE = {15, 87, 139}
|
|
335
|
+
XL_CHART_TYPE_LINE = {4, 127, 65, 66, 67, 63, 64, 128, 129}
|
exstruct/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import tempfile
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
import xlwings as xw
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _require_excel_app() -> xw.App:
|
|
15
|
+
"""Ensure Excel COM is available and return an App; otherwise raise."""
|
|
16
|
+
try:
|
|
17
|
+
app = xw.App(add_book=False, visible=False)
|
|
18
|
+
return app
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise RuntimeError(
|
|
21
|
+
"Excel (COM) is not available. Rendering (PDF/image) requires a desktop Excel installation."
|
|
22
|
+
) from e
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def export_pdf(excel_path: Path, output_pdf: Path) -> List[str]:
|
|
26
|
+
"""Export an Excel workbook to PDF via Excel COM and return sheet names in order."""
|
|
27
|
+
output_pdf.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
with tempfile.TemporaryDirectory() as td:
|
|
30
|
+
temp_dir = Path(td)
|
|
31
|
+
temp_xlsx = temp_dir / "book.xlsx"
|
|
32
|
+
temp_pdf = temp_dir / "book.pdf"
|
|
33
|
+
shutil.copy(excel_path, temp_xlsx)
|
|
34
|
+
|
|
35
|
+
app = _require_excel_app()
|
|
36
|
+
try:
|
|
37
|
+
wb = app.books.open(str(temp_xlsx))
|
|
38
|
+
except Exception:
|
|
39
|
+
app.quit()
|
|
40
|
+
raise
|
|
41
|
+
try:
|
|
42
|
+
sheet_names = [s.name for s in wb.sheets]
|
|
43
|
+
wb.api.ExportAsFixedFormat(0, str(temp_pdf))
|
|
44
|
+
shutil.copy(temp_pdf, output_pdf)
|
|
45
|
+
finally:
|
|
46
|
+
wb.close()
|
|
47
|
+
app.quit()
|
|
48
|
+
return sheet_names
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _require_pdfium():
|
|
52
|
+
"""Ensure pypdfium2 is installed; otherwise raise with guidance."""
|
|
53
|
+
try:
|
|
54
|
+
import pypdfium2 as pdfium # type: ignore
|
|
55
|
+
except ImportError as e:
|
|
56
|
+
raise RuntimeError(
|
|
57
|
+
"Image rendering requires pypdfium2. Install it via `pip install pypdfium2` or add the 'render' extra."
|
|
58
|
+
) from e
|
|
59
|
+
return pdfium
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def export_sheet_images(excel_path: Path, output_dir: Path, dpi: int = 144) -> List[Path]:
|
|
63
|
+
"""Export each sheet as PNG (via PDF then pypdfium2 rasterization) and return paths in sheet order."""
|
|
64
|
+
pdfium = _require_pdfium()
|
|
65
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
with tempfile.TemporaryDirectory() as td:
|
|
68
|
+
tmp_pdf = Path(td) / "book.pdf"
|
|
69
|
+
sheet_names = export_pdf(excel_path, tmp_pdf)
|
|
70
|
+
|
|
71
|
+
scale = dpi / 72.0
|
|
72
|
+
written: List[Path] = []
|
|
73
|
+
with pdfium.PdfDocument(str(tmp_pdf)) as pdf: # type: ignore
|
|
74
|
+
for i, sheet_name in enumerate(sheet_names):
|
|
75
|
+
page = pdf[i]
|
|
76
|
+
bitmap = page.render(scale=scale) # type: ignore
|
|
77
|
+
pil_image = bitmap.to_pil() # type: ignore
|
|
78
|
+
safe_name = _sanitize_sheet_filename(sheet_name)
|
|
79
|
+
img_path = output_dir / f"{i+1:02d}_{safe_name}.png"
|
|
80
|
+
pil_image.save(img_path, format="PNG", dpi=(dpi, dpi))
|
|
81
|
+
written.append(img_path)
|
|
82
|
+
return written
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _sanitize_sheet_filename(name: str) -> str:
|
|
86
|
+
return "".join("_" if c in '\\/:*?"<>|' else c for c in name).strip() or "sheet"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["export_pdf", "export_sheet_images"]
|