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.
@@ -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]
@@ -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"]