aspose-cells-foss 25.12.1__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.
- aspose/__init__.py +14 -0
- aspose/cells/__init__.py +31 -0
- aspose/cells/cell.py +350 -0
- aspose/cells/constants.py +44 -0
- aspose/cells/converters/__init__.py +13 -0
- aspose/cells/converters/csv_converter.py +55 -0
- aspose/cells/converters/json_converter.py +46 -0
- aspose/cells/converters/markdown_converter.py +453 -0
- aspose/cells/drawing/__init__.py +17 -0
- aspose/cells/drawing/anchor.py +172 -0
- aspose/cells/drawing/collection.py +233 -0
- aspose/cells/drawing/image.py +338 -0
- aspose/cells/formats.py +80 -0
- aspose/cells/formula/__init__.py +10 -0
- aspose/cells/formula/evaluator.py +360 -0
- aspose/cells/formula/functions.py +433 -0
- aspose/cells/formula/tokenizer.py +340 -0
- aspose/cells/io/__init__.py +27 -0
- aspose/cells/io/csv/__init__.py +8 -0
- aspose/cells/io/csv/reader.py +88 -0
- aspose/cells/io/csv/writer.py +98 -0
- aspose/cells/io/factory.py +138 -0
- aspose/cells/io/interfaces.py +48 -0
- aspose/cells/io/json/__init__.py +8 -0
- aspose/cells/io/json/reader.py +126 -0
- aspose/cells/io/json/writer.py +119 -0
- aspose/cells/io/md/__init__.py +8 -0
- aspose/cells/io/md/reader.py +161 -0
- aspose/cells/io/md/writer.py +334 -0
- aspose/cells/io/models.py +64 -0
- aspose/cells/io/xlsx/__init__.py +9 -0
- aspose/cells/io/xlsx/constants.py +312 -0
- aspose/cells/io/xlsx/image_writer.py +311 -0
- aspose/cells/io/xlsx/reader.py +284 -0
- aspose/cells/io/xlsx/writer.py +931 -0
- aspose/cells/plugins/__init__.py +6 -0
- aspose/cells/plugins/docling_backend/__init__.py +7 -0
- aspose/cells/plugins/docling_backend/backend.py +535 -0
- aspose/cells/plugins/markitdown_plugin/__init__.py +15 -0
- aspose/cells/plugins/markitdown_plugin/plugin.py +128 -0
- aspose/cells/range.py +210 -0
- aspose/cells/style.py +287 -0
- aspose/cells/utils/__init__.py +54 -0
- aspose/cells/utils/coordinates.py +68 -0
- aspose/cells/utils/exceptions.py +43 -0
- aspose/cells/utils/validation.py +102 -0
- aspose/cells/workbook.py +352 -0
- aspose/cells/worksheet.py +670 -0
- aspose_cells_foss-25.12.1.dist-info/METADATA +189 -0
- aspose_cells_foss-25.12.1.dist-info/RECORD +53 -0
- aspose_cells_foss-25.12.1.dist-info/WHEEL +5 -0
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +2 -0
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worksheet implementation with Pythonic cell access and data operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Iterator, List, Optional, Tuple, Union, TYPE_CHECKING
|
|
6
|
+
from .cell import Cell
|
|
7
|
+
from .range import Range
|
|
8
|
+
from .formats import CellValue
|
|
9
|
+
from .drawing import ImageCollection, Image, ImageFormat
|
|
10
|
+
from .utils import (
|
|
11
|
+
coordinate_to_tuple,
|
|
12
|
+
sanitize_sheet_name,
|
|
13
|
+
InvalidCoordinateError,
|
|
14
|
+
WorksheetNotFoundError
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .workbook import Workbook
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Worksheet:
|
|
22
|
+
"""Excel worksheet with multiple access patterns and batch operations."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, parent: 'Workbook', name: str):
|
|
25
|
+
self._parent = parent
|
|
26
|
+
self._name = sanitize_sheet_name(name)
|
|
27
|
+
self._cells: Dict[Tuple[int, int], Cell] = {}
|
|
28
|
+
self._max_row = 0
|
|
29
|
+
self._max_column = 0
|
|
30
|
+
self._merged_ranges: set = set()
|
|
31
|
+
self._row_heights: Dict[int, float] = {}
|
|
32
|
+
self._column_widths: Dict[int, float] = {}
|
|
33
|
+
self._hidden_rows: set = set()
|
|
34
|
+
self._hidden_columns: set = set()
|
|
35
|
+
self._freeze_panes: Optional[str] = None
|
|
36
|
+
self._images: ImageCollection = ImageCollection(self)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def name(self) -> str:
|
|
40
|
+
"""Worksheet name."""
|
|
41
|
+
return self._name
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def workbook(self) -> 'Workbook':
|
|
45
|
+
"""Parent workbook."""
|
|
46
|
+
return self._parent
|
|
47
|
+
|
|
48
|
+
@name.setter
|
|
49
|
+
def name(self, value: str):
|
|
50
|
+
"""Set worksheet name with validation."""
|
|
51
|
+
new_name = sanitize_sheet_name(value)
|
|
52
|
+
if new_name in self._parent._worksheets and new_name != self._name:
|
|
53
|
+
raise WorksheetNotFoundError(f"Worksheet '{new_name}' already exists")
|
|
54
|
+
|
|
55
|
+
# Update parent's worksheet mapping
|
|
56
|
+
if self._name in self._parent._worksheets:
|
|
57
|
+
del self._parent._worksheets[self._name]
|
|
58
|
+
self._parent._worksheets[new_name] = self
|
|
59
|
+
self._name = new_name
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def max_row(self) -> int:
|
|
63
|
+
"""Maximum row with data."""
|
|
64
|
+
return self._max_row
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def max_column(self) -> int:
|
|
68
|
+
"""Maximum column with data."""
|
|
69
|
+
return self._max_column
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def dimensions(self) -> str:
|
|
73
|
+
"""Range representing used area."""
|
|
74
|
+
if self._max_row == 0 or self._max_column == 0:
|
|
75
|
+
return "A1:A1"
|
|
76
|
+
|
|
77
|
+
from .utils import tuple_to_coordinate
|
|
78
|
+
start = tuple_to_coordinate(1, 1)
|
|
79
|
+
end = tuple_to_coordinate(self._max_row, self._max_column)
|
|
80
|
+
return f"{start}:{end}"
|
|
81
|
+
|
|
82
|
+
def _update_bounds(self, row: int, column: int):
|
|
83
|
+
"""Update worksheet bounds when cell is modified."""
|
|
84
|
+
self._max_row = max(self._max_row, row)
|
|
85
|
+
self._max_column = max(self._max_column, column)
|
|
86
|
+
|
|
87
|
+
def __getitem__(self, key: Union[str, Tuple[int, int]]) -> Union[Cell, Range]:
|
|
88
|
+
"""Access cell or range using Excel coordinates or 0-based tuples."""
|
|
89
|
+
if isinstance(key, str):
|
|
90
|
+
if ':' in key:
|
|
91
|
+
# Range access: ws['A1:C3']
|
|
92
|
+
return Range(self, key)
|
|
93
|
+
else:
|
|
94
|
+
# Single cell: ws['A1'] (Excel-style)
|
|
95
|
+
row, col = coordinate_to_tuple(key)
|
|
96
|
+
return self.cell(row, col)
|
|
97
|
+
elif isinstance(key, tuple) and len(key) == 2:
|
|
98
|
+
# 0-based tuple access: ws[0, 0] -> A1
|
|
99
|
+
row, col = key
|
|
100
|
+
return self.cell(row + 1, col + 1) # Convert to 1-based internally
|
|
101
|
+
else:
|
|
102
|
+
raise InvalidCoordinateError(f"Invalid cell access pattern: {key}")
|
|
103
|
+
|
|
104
|
+
def __setitem__(self, key: Union[str, Tuple[int, int]], value: CellValue):
|
|
105
|
+
"""Set cell value using Excel coordinates or 0-based tuples."""
|
|
106
|
+
if isinstance(key, str) and ':' in key:
|
|
107
|
+
# Range assignment: ws['A1:C3'] = data
|
|
108
|
+
range_obj = Range(self, key)
|
|
109
|
+
range_obj.values = value
|
|
110
|
+
elif isinstance(key, tuple) and len(key) == 2:
|
|
111
|
+
# 0-based tuple: ws[0, 0] = value -> A1
|
|
112
|
+
row, col = key
|
|
113
|
+
self.cell(row + 1, col + 1).value = value
|
|
114
|
+
elif isinstance(key, str):
|
|
115
|
+
# Excel coordinate: ws['A1'] = value
|
|
116
|
+
cell = self[key]
|
|
117
|
+
cell.value = value
|
|
118
|
+
else:
|
|
119
|
+
raise InvalidCoordinateError(f"Invalid cell assignment pattern: {key}")
|
|
120
|
+
|
|
121
|
+
def cell(self, row: int, column: int, value: CellValue = None) -> Cell:
|
|
122
|
+
"""Get or create cell at specified position (1-based)."""
|
|
123
|
+
if row < 1 or column < 1:
|
|
124
|
+
raise InvalidCoordinateError(f"Row and column must be >= 1, got ({row}, {column})")
|
|
125
|
+
|
|
126
|
+
coord = (row, column)
|
|
127
|
+
|
|
128
|
+
if coord not in self._cells:
|
|
129
|
+
self._cells[coord] = Cell(self, row, column)
|
|
130
|
+
self._update_bounds(row, column)
|
|
131
|
+
|
|
132
|
+
if value is not None:
|
|
133
|
+
self._cells[coord].value = value
|
|
134
|
+
|
|
135
|
+
return self._cells[coord]
|
|
136
|
+
|
|
137
|
+
def append(self, iterable: List[CellValue]):
|
|
138
|
+
"""Add row of data to end of worksheet (like list.append)."""
|
|
139
|
+
if not iterable:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
row = self._max_row + 1
|
|
143
|
+
for col, value in enumerate(iterable, 1):
|
|
144
|
+
if value is not None: # Skip None values to save memory
|
|
145
|
+
self.cell(row, col, value)
|
|
146
|
+
|
|
147
|
+
def extend(self, data: List[List[CellValue]]):
|
|
148
|
+
"""Add multiple rows of data (like list.extend)."""
|
|
149
|
+
for row_data in data:
|
|
150
|
+
self.append(row_data)
|
|
151
|
+
|
|
152
|
+
def insert(self, index: int, iterable: List[CellValue]):
|
|
153
|
+
"""Insert row at specified position (like list.insert)."""
|
|
154
|
+
if index < 1:
|
|
155
|
+
index = 1
|
|
156
|
+
|
|
157
|
+
# Shift existing data down
|
|
158
|
+
cells_to_move = []
|
|
159
|
+
for coord, cell in self._cells.items():
|
|
160
|
+
row, col = coord
|
|
161
|
+
if row >= index:
|
|
162
|
+
cells_to_move.append((coord, cell))
|
|
163
|
+
|
|
164
|
+
# Remove old cells and create new ones
|
|
165
|
+
for old_coord, cell in cells_to_move:
|
|
166
|
+
old_row, col = old_coord
|
|
167
|
+
del self._cells[old_coord]
|
|
168
|
+
new_cell = Cell(self, old_row + 1, col, cell.value)
|
|
169
|
+
if cell._style:
|
|
170
|
+
new_cell._style = cell._style.copy()
|
|
171
|
+
new_cell._number_format = cell._number_format
|
|
172
|
+
self._cells[(old_row + 1, col)] = new_cell
|
|
173
|
+
|
|
174
|
+
# Insert new row
|
|
175
|
+
for col, value in enumerate(iterable, 1):
|
|
176
|
+
if value is not None:
|
|
177
|
+
self.cell(index, col, value)
|
|
178
|
+
|
|
179
|
+
def from_records(self, records: List[Dict[str, CellValue]], include_headers: bool = True):
|
|
180
|
+
"""Import data from list of dictionaries (like pandas.from_records)."""
|
|
181
|
+
if not records:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Get column names
|
|
185
|
+
keys = list(records[0].keys())
|
|
186
|
+
|
|
187
|
+
start_row = self._max_row + 1
|
|
188
|
+
|
|
189
|
+
# Add headers if requested
|
|
190
|
+
if include_headers:
|
|
191
|
+
self.append(keys)
|
|
192
|
+
|
|
193
|
+
# Add data rows
|
|
194
|
+
for record in records:
|
|
195
|
+
row_data = [record.get(key) for key in keys]
|
|
196
|
+
self.append(row_data)
|
|
197
|
+
|
|
198
|
+
def rows(self) -> Iterator[List[Cell]]:
|
|
199
|
+
"""Iterate over all rows with data."""
|
|
200
|
+
for row_idx in range(1, self.max_row + 1):
|
|
201
|
+
row_cells = []
|
|
202
|
+
for col_idx in range(1, self.max_column + 1):
|
|
203
|
+
coord = (row_idx, col_idx)
|
|
204
|
+
if coord in self._cells:
|
|
205
|
+
row_cells.append(self._cells[coord])
|
|
206
|
+
else:
|
|
207
|
+
# Create empty cell for iteration
|
|
208
|
+
row_cells.append(Cell(self, row_idx, col_idx))
|
|
209
|
+
yield row_cells
|
|
210
|
+
|
|
211
|
+
def columns(self) -> Iterator[List[Cell]]:
|
|
212
|
+
"""Iterate over all columns with data."""
|
|
213
|
+
for col_idx in range(1, self.max_column + 1):
|
|
214
|
+
col_cells = []
|
|
215
|
+
for row_idx in range(1, self.max_row + 1):
|
|
216
|
+
coord = (row_idx, col_idx)
|
|
217
|
+
if coord in self._cells:
|
|
218
|
+
col_cells.append(self._cells[coord])
|
|
219
|
+
else:
|
|
220
|
+
col_cells.append(Cell(self, row_idx, col_idx))
|
|
221
|
+
yield col_cells
|
|
222
|
+
|
|
223
|
+
def iter_rows(self, min_row: int = 1, max_row: Optional[int] = None,
|
|
224
|
+
min_col: int = 1, max_col: Optional[int] = None) -> Iterator[List[Cell]]:
|
|
225
|
+
"""Iterate over specified range of rows."""
|
|
226
|
+
if max_row is None:
|
|
227
|
+
max_row = self.max_row or 1
|
|
228
|
+
if max_col is None:
|
|
229
|
+
max_col = self.max_column or 1
|
|
230
|
+
|
|
231
|
+
for row_idx in range(min_row, max_row + 1):
|
|
232
|
+
row_cells = []
|
|
233
|
+
for col_idx in range(min_col, max_col + 1):
|
|
234
|
+
coord = (row_idx, col_idx)
|
|
235
|
+
if coord in self._cells:
|
|
236
|
+
row_cells.append(self._cells[coord])
|
|
237
|
+
else:
|
|
238
|
+
row_cells.append(Cell(self, row_idx, col_idx))
|
|
239
|
+
yield row_cells
|
|
240
|
+
|
|
241
|
+
def iter_cols(self, min_row: int = 1, max_row: Optional[int] = None,
|
|
242
|
+
min_col: int = 1, max_col: Optional[int] = None) -> Iterator[List[Cell]]:
|
|
243
|
+
"""Iterate over specified range of columns."""
|
|
244
|
+
if max_row is None:
|
|
245
|
+
max_row = self.max_row or 1
|
|
246
|
+
if max_col is None:
|
|
247
|
+
max_col = self.max_column or 1
|
|
248
|
+
|
|
249
|
+
for col_idx in range(min_col, max_col + 1):
|
|
250
|
+
col_cells = []
|
|
251
|
+
for row_idx in range(min_row, max_row + 1):
|
|
252
|
+
coord = (row_idx, col_idx)
|
|
253
|
+
if coord in self._cells:
|
|
254
|
+
col_cells.append(self._cells[coord])
|
|
255
|
+
else:
|
|
256
|
+
col_cells.append(Cell(self, row_idx, col_idx))
|
|
257
|
+
yield col_cells
|
|
258
|
+
|
|
259
|
+
def merge_cells(self, range_string: str):
|
|
260
|
+
"""Merge cells in specified range."""
|
|
261
|
+
self._merged_ranges.add(range_string.upper())
|
|
262
|
+
|
|
263
|
+
def unmerge_cells(self, range_string: str):
|
|
264
|
+
"""Unmerge previously merged cells."""
|
|
265
|
+
self._merged_ranges.discard(range_string.upper())
|
|
266
|
+
|
|
267
|
+
def delete_rows(self, idx: int, amount: int = 1):
|
|
268
|
+
"""Delete specified number of rows."""
|
|
269
|
+
for _ in range(amount):
|
|
270
|
+
# Remove cells in row
|
|
271
|
+
cells_to_remove = [(row, col) for row, col in self._cells.keys() if row == idx]
|
|
272
|
+
for coord in cells_to_remove:
|
|
273
|
+
del self._cells[coord]
|
|
274
|
+
|
|
275
|
+
# Shift rows up
|
|
276
|
+
cells_to_move = [(row, col) for row, col in self._cells.keys() if row > idx]
|
|
277
|
+
for old_row, col in cells_to_move:
|
|
278
|
+
cell = self._cells.pop((old_row, col))
|
|
279
|
+
cell._row = old_row - 1
|
|
280
|
+
self._cells[(old_row - 1, col)] = cell
|
|
281
|
+
|
|
282
|
+
# Update max_row
|
|
283
|
+
if self._cells:
|
|
284
|
+
self._max_row = max(row for row, col in self._cells.keys())
|
|
285
|
+
else:
|
|
286
|
+
self._max_row = 0
|
|
287
|
+
|
|
288
|
+
def delete_cols(self, idx: int, amount: int = 1):
|
|
289
|
+
"""Delete specified number of columns."""
|
|
290
|
+
for _ in range(amount):
|
|
291
|
+
# Remove cells in column
|
|
292
|
+
cells_to_remove = [(row, col) for row, col in self._cells.keys() if col == idx]
|
|
293
|
+
for coord in cells_to_remove:
|
|
294
|
+
del self._cells[coord]
|
|
295
|
+
|
|
296
|
+
# Shift columns left
|
|
297
|
+
cells_to_move = [(row, col) for row, col in self._cells.keys() if col > idx]
|
|
298
|
+
for row, old_col in cells_to_move:
|
|
299
|
+
cell = self._cells.pop((row, old_col))
|
|
300
|
+
cell._column = old_col - 1
|
|
301
|
+
self._cells[(row, old_col - 1)] = cell
|
|
302
|
+
|
|
303
|
+
# Update max_column
|
|
304
|
+
if self._cells:
|
|
305
|
+
self._max_column = max(col for row, col in self._cells.keys())
|
|
306
|
+
else:
|
|
307
|
+
self._max_column = 0
|
|
308
|
+
|
|
309
|
+
def freeze_panes(self, cell: Union[str, Cell, None] = None):
|
|
310
|
+
"""Freeze panes at specified cell."""
|
|
311
|
+
if cell is None:
|
|
312
|
+
self._freeze_panes = None
|
|
313
|
+
elif isinstance(cell, str):
|
|
314
|
+
self._freeze_panes = cell.upper()
|
|
315
|
+
elif isinstance(cell, Cell):
|
|
316
|
+
self._freeze_panes = cell.coordinate
|
|
317
|
+
else:
|
|
318
|
+
raise InvalidCoordinateError(f"Invalid freeze panes cell: {cell}")
|
|
319
|
+
|
|
320
|
+
def __str__(self) -> str:
|
|
321
|
+
"""String representation."""
|
|
322
|
+
return f"Worksheet('{self._name}')"
|
|
323
|
+
|
|
324
|
+
def __repr__(self) -> str:
|
|
325
|
+
"""Debug representation."""
|
|
326
|
+
return f"Worksheet(name='{self._name}', max_row={self._max_row}, max_col={self._max_column})"
|
|
327
|
+
|
|
328
|
+
# Column width and row height functionality
|
|
329
|
+
def set_column_width(self, column: Union[int, str], width: float):
|
|
330
|
+
"""Set width for a specific column (0-based int or Excel letter)."""
|
|
331
|
+
if isinstance(column, str):
|
|
332
|
+
# Convert letter to 0-based number (A=0, B=1, etc.)
|
|
333
|
+
col_num = ord(column.upper()) - ord('A')
|
|
334
|
+
else:
|
|
335
|
+
# 0-based integer column index
|
|
336
|
+
col_num = column
|
|
337
|
+
|
|
338
|
+
if col_num < 0:
|
|
339
|
+
raise InvalidCoordinateError(f"Column must be >= 0, got {col_num}")
|
|
340
|
+
|
|
341
|
+
# Store internally as 1-based for compatibility
|
|
342
|
+
self._column_widths[col_num + 1] = width
|
|
343
|
+
|
|
344
|
+
def get_column_width(self, column: Union[int, str]) -> float:
|
|
345
|
+
"""Get width for a specific column (0-based int or Excel letter)."""
|
|
346
|
+
if isinstance(column, str):
|
|
347
|
+
# Convert letter to 0-based number (A=0, B=1, etc.)
|
|
348
|
+
col_num = ord(column.upper()) - ord('A')
|
|
349
|
+
else:
|
|
350
|
+
# 0-based integer column index
|
|
351
|
+
col_num = column
|
|
352
|
+
|
|
353
|
+
# Retrieve using 1-based internal storage
|
|
354
|
+
return self._column_widths.get(col_num + 1, 10.0) # Default width
|
|
355
|
+
|
|
356
|
+
def set_row_height(self, row: int, height: float):
|
|
357
|
+
"""Set height for a specific row (0-based)."""
|
|
358
|
+
if row < 0:
|
|
359
|
+
raise InvalidCoordinateError(f"Row must be >= 0, got {row}")
|
|
360
|
+
|
|
361
|
+
# Store internally as 1-based for compatibility
|
|
362
|
+
self._row_heights[row + 1] = height
|
|
363
|
+
|
|
364
|
+
def get_row_height(self, row: int) -> float:
|
|
365
|
+
"""Get height for a specific row (0-based)."""
|
|
366
|
+
# Retrieve using 1-based internal storage
|
|
367
|
+
return self._row_heights.get(row + 1, 15.0) # Default height
|
|
368
|
+
|
|
369
|
+
def auto_size_column(self, column: Union[int, str]):
|
|
370
|
+
"""Auto-size column based on content (0-based int or Excel letter)."""
|
|
371
|
+
if isinstance(column, str):
|
|
372
|
+
# Convert letter to 0-based number
|
|
373
|
+
col_num = ord(column.upper()) - ord('A')
|
|
374
|
+
else:
|
|
375
|
+
# 0-based integer column index
|
|
376
|
+
col_num = column
|
|
377
|
+
|
|
378
|
+
max_width = 10.0 # Default minimum
|
|
379
|
+
|
|
380
|
+
# Calculate based on cell content (internal storage is 1-based)
|
|
381
|
+
internal_col = col_num + 1
|
|
382
|
+
for (row, col), cell in self._cells.items():
|
|
383
|
+
if col == internal_col and cell.value is not None:
|
|
384
|
+
content_length = len(str(cell.value))
|
|
385
|
+
estimated_width = min(content_length * 1.2, 50.0) # Max width 50
|
|
386
|
+
max_width = max(max_width, estimated_width)
|
|
387
|
+
|
|
388
|
+
self._column_widths[internal_col] = max_width
|
|
389
|
+
|
|
390
|
+
def set_cell_style(self, coordinate: Union[str, Tuple[int, int]], **style_kwargs):
|
|
391
|
+
"""Set cell style with convenient keyword arguments."""
|
|
392
|
+
cell = self[coordinate]
|
|
393
|
+
|
|
394
|
+
# Font properties
|
|
395
|
+
if 'font_name' in style_kwargs:
|
|
396
|
+
cell.font.name = style_kwargs['font_name']
|
|
397
|
+
if 'font_size' in style_kwargs:
|
|
398
|
+
cell.font.size = style_kwargs['font_size']
|
|
399
|
+
if 'bold' in style_kwargs:
|
|
400
|
+
cell.font.bold = style_kwargs['bold']
|
|
401
|
+
if 'italic' in style_kwargs:
|
|
402
|
+
cell.font.italic = style_kwargs['italic']
|
|
403
|
+
if 'font_color' in style_kwargs:
|
|
404
|
+
cell.font.color = style_kwargs['font_color']
|
|
405
|
+
|
|
406
|
+
# Fill properties
|
|
407
|
+
if 'fill_color' in style_kwargs:
|
|
408
|
+
cell.fill.color = style_kwargs['fill_color']
|
|
409
|
+
|
|
410
|
+
# Number format
|
|
411
|
+
if 'number_format' in style_kwargs:
|
|
412
|
+
cell.number_format = style_kwargs['number_format']
|
|
413
|
+
|
|
414
|
+
# Alignment
|
|
415
|
+
if 'horizontal' in style_kwargs:
|
|
416
|
+
cell.alignment.horizontal = style_kwargs['horizontal']
|
|
417
|
+
if 'vertical' in style_kwargs:
|
|
418
|
+
cell.alignment.vertical = style_kwargs['vertical']
|
|
419
|
+
|
|
420
|
+
def set_range_style(self, range_str: str, **style_kwargs):
|
|
421
|
+
"""Set style for entire range with convenient keyword arguments."""
|
|
422
|
+
range_obj = self[range_str]
|
|
423
|
+
for cell in range_obj:
|
|
424
|
+
self.set_cell_style(cell.coordinate, **style_kwargs)
|
|
425
|
+
|
|
426
|
+
def populate_data(self, start_cell: Union[str, Tuple[int, int]], data: List[List],
|
|
427
|
+
column_styles: Dict[int, Dict] = None, conditional_styles: Dict = None):
|
|
428
|
+
"""Populate data with automatic styling based on column and conditions.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
start_cell: Starting cell coordinate
|
|
432
|
+
data: 2D list of data to populate
|
|
433
|
+
column_styles: Dict mapping column index (0-based) to style kwargs
|
|
434
|
+
conditional_styles: Dict with condition functions and styles
|
|
435
|
+
"""
|
|
436
|
+
if isinstance(start_cell, str):
|
|
437
|
+
from .utils import coordinate_to_tuple
|
|
438
|
+
start_row, start_col = coordinate_to_tuple(start_cell)
|
|
439
|
+
else:
|
|
440
|
+
start_row, start_col = start_cell[0] + 1, start_cell[1] + 1 # Convert to 1-based
|
|
441
|
+
|
|
442
|
+
for row_offset, row_data in enumerate(data):
|
|
443
|
+
for col_offset, value in enumerate(row_data):
|
|
444
|
+
current_row = start_row + row_offset
|
|
445
|
+
current_col = start_col + col_offset
|
|
446
|
+
|
|
447
|
+
# Set the value
|
|
448
|
+
cell = self.cell(current_row, current_col, value)
|
|
449
|
+
|
|
450
|
+
# Apply column-specific styles
|
|
451
|
+
if column_styles and col_offset in column_styles:
|
|
452
|
+
coord = (current_row - 1, current_col - 1) # Convert to 0-based for style method
|
|
453
|
+
self.set_cell_style(coord, **column_styles[col_offset])
|
|
454
|
+
|
|
455
|
+
# Apply conditional styles
|
|
456
|
+
if conditional_styles:
|
|
457
|
+
for condition_name, condition_config in conditional_styles.items():
|
|
458
|
+
condition_func = condition_config['condition']
|
|
459
|
+
if condition_func(value, row_offset, col_offset):
|
|
460
|
+
coord = (current_row - 1, current_col - 1)
|
|
461
|
+
style_dict = condition_config['style']
|
|
462
|
+
# Handle both static dict and function that returns dict
|
|
463
|
+
if callable(style_dict):
|
|
464
|
+
style_dict = style_dict(value)
|
|
465
|
+
self.set_cell_style(coord, **style_dict)
|
|
466
|
+
|
|
467
|
+
def apply_column_formats(self, start_col: int, formats: List[str]):
|
|
468
|
+
"""Apply number formats to consecutive columns.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
start_col: Starting column index (0-based)
|
|
472
|
+
formats: List of number format strings
|
|
473
|
+
"""
|
|
474
|
+
for i, fmt in enumerate(formats):
|
|
475
|
+
col_idx = start_col + i
|
|
476
|
+
# Only iterate over rows that have data in this column
|
|
477
|
+
for coord, cell in self._cells.items():
|
|
478
|
+
row_idx, cell_col_idx = coord
|
|
479
|
+
if cell_col_idx == col_idx:
|
|
480
|
+
try:
|
|
481
|
+
self.set_cell_style(coord, number_format=fmt)
|
|
482
|
+
except (KeyError, ValueError, TypeError):
|
|
483
|
+
# Skip cells that have invalid coordinates
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
def create_table(self, start_cell: Union[str, Tuple[int, int]],
|
|
487
|
+
headers: List[str], data: List[List],
|
|
488
|
+
header_style: Dict = None,
|
|
489
|
+
column_styles: Dict[int, Dict] = None,
|
|
490
|
+
conditional_styles: Dict = None,
|
|
491
|
+
auto_width: bool = True):
|
|
492
|
+
"""Create a complete table with headers, data, and styling in one call.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
start_cell: Starting cell coordinate
|
|
496
|
+
headers: List of header names
|
|
497
|
+
data: 2D list of table data
|
|
498
|
+
header_style: Style dict for header row
|
|
499
|
+
column_styles: Dict mapping column index to style kwargs
|
|
500
|
+
conditional_styles: Dict with conditional formatting rules
|
|
501
|
+
auto_width: Whether to auto-size columns
|
|
502
|
+
"""
|
|
503
|
+
if isinstance(start_cell, str):
|
|
504
|
+
from .utils import coordinate_to_tuple
|
|
505
|
+
start_row, start_col = coordinate_to_tuple(start_cell)
|
|
506
|
+
else:
|
|
507
|
+
start_row, start_col = start_cell[0] + 1, start_cell[1] + 1
|
|
508
|
+
|
|
509
|
+
# Add headers
|
|
510
|
+
for col_offset, header in enumerate(headers):
|
|
511
|
+
cell = self.cell(start_row, start_col + col_offset, header)
|
|
512
|
+
if header_style:
|
|
513
|
+
coord = (start_row - 1, start_col + col_offset - 1)
|
|
514
|
+
self.set_cell_style(coord, **header_style)
|
|
515
|
+
|
|
516
|
+
# Add data with styles
|
|
517
|
+
if data:
|
|
518
|
+
data_start = (start_row, start_col - 1) # Adjust for populate_data
|
|
519
|
+
self.populate_data(data_start, data, column_styles, conditional_styles)
|
|
520
|
+
|
|
521
|
+
# Auto-size columns if requested
|
|
522
|
+
if auto_width:
|
|
523
|
+
for i in range(len(headers)):
|
|
524
|
+
try:
|
|
525
|
+
self.auto_size_column(start_col - 1 + i) # Convert to 0-based
|
|
526
|
+
except (IndexError, ValueError):
|
|
527
|
+
# Skip invalid column indices
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
# Image-related methods and properties
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def images(self) -> ImageCollection:
|
|
534
|
+
"""Get image collection for this worksheet."""
|
|
535
|
+
return self._images
|
|
536
|
+
|
|
537
|
+
def add_image(self, source, cell_ref: str = "A1",
|
|
538
|
+
offset: Tuple[int, int] = (0, 0),
|
|
539
|
+
name: Optional[str] = None,
|
|
540
|
+
format: Optional[ImageFormat] = None) -> Image:
|
|
541
|
+
"""
|
|
542
|
+
Add image to worksheet at specified cell position.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
source: Image source (file path, bytes, PIL Image, etc.)
|
|
546
|
+
cell_ref: Cell reference for positioning (default: "A1")
|
|
547
|
+
offset: Pixel offset from cell (default: (0, 0))
|
|
548
|
+
name: Optional image name
|
|
549
|
+
format: Optional format override
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
Image: Added image object
|
|
553
|
+
|
|
554
|
+
Example:
|
|
555
|
+
# Add image from file
|
|
556
|
+
img = ws.add_image('logo.png', 'B2')
|
|
557
|
+
|
|
558
|
+
# Add with offset
|
|
559
|
+
img = ws.add_image('chart.jpg', 'D5', offset=(10, 5))
|
|
560
|
+
|
|
561
|
+
# Add with custom name
|
|
562
|
+
img = ws.add_image('data.png', 'A1', name='DataChart')
|
|
563
|
+
"""
|
|
564
|
+
return self._images.add(source, cell_ref, offset, name, format)
|
|
565
|
+
|
|
566
|
+
def add_image_to_range(self, source, start_cell: str, end_cell: str,
|
|
567
|
+
start_offset: Tuple[int, int] = (0, 0),
|
|
568
|
+
end_offset: Tuple[int, int] = (0, 0),
|
|
569
|
+
name: Optional[str] = None,
|
|
570
|
+
format: Optional[ImageFormat] = None) -> Image:
|
|
571
|
+
"""
|
|
572
|
+
Add image positioned to span a cell range.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
source: Image source
|
|
576
|
+
start_cell: Starting cell reference
|
|
577
|
+
end_cell: Ending cell reference
|
|
578
|
+
start_offset: Pixel offset from start cell
|
|
579
|
+
end_offset: Pixel offset from end cell
|
|
580
|
+
name: Optional image name
|
|
581
|
+
format: Optional format override
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Image: Added image object
|
|
585
|
+
|
|
586
|
+
Example:
|
|
587
|
+
# Span image across range
|
|
588
|
+
img = ws.add_image_to_range('banner.png', 'A1', 'E3')
|
|
589
|
+
"""
|
|
590
|
+
return self._images.add_at_range(
|
|
591
|
+
source, start_cell, end_cell,
|
|
592
|
+
start_offset, end_offset, name, format
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def add_image_absolute(self, source, x: int, y: int,
|
|
596
|
+
name: Optional[str] = None,
|
|
597
|
+
format: Optional[ImageFormat] = None) -> Image:
|
|
598
|
+
"""
|
|
599
|
+
Add image at absolute position (independent of cells).
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
source: Image source
|
|
603
|
+
x: X coordinate in pixels
|
|
604
|
+
y: Y coordinate in pixels
|
|
605
|
+
name: Optional image name
|
|
606
|
+
format: Optional format override
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Image: Added image object
|
|
610
|
+
|
|
611
|
+
Example:
|
|
612
|
+
# Add at exact position
|
|
613
|
+
img = ws.add_image_absolute('watermark.png', 100, 50)
|
|
614
|
+
"""
|
|
615
|
+
return self._images.add_absolute(source, x, y, name, format)
|
|
616
|
+
|
|
617
|
+
def remove_image(self, target):
|
|
618
|
+
"""
|
|
619
|
+
Remove image by name, index, or object.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
target: Image identifier (name, index, or Image object)
|
|
623
|
+
|
|
624
|
+
Example:
|
|
625
|
+
# Remove by name
|
|
626
|
+
ws.remove_image('DataChart')
|
|
627
|
+
|
|
628
|
+
# Remove by index
|
|
629
|
+
ws.remove_image(0)
|
|
630
|
+
|
|
631
|
+
# Remove by object
|
|
632
|
+
ws.remove_image(img)
|
|
633
|
+
"""
|
|
634
|
+
self._images.remove(target)
|
|
635
|
+
|
|
636
|
+
def get_image(self, identifier) -> Image:
|
|
637
|
+
"""
|
|
638
|
+
Get image by name or index.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
identifier: Image name or index
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
Image: Found image object
|
|
645
|
+
|
|
646
|
+
Example:
|
|
647
|
+
img = ws.get_image('DataChart')
|
|
648
|
+
img = ws.get_image(0)
|
|
649
|
+
"""
|
|
650
|
+
return self._images.get(identifier)
|
|
651
|
+
|
|
652
|
+
def get_images_at(self, cell_ref: str) -> List[Image]:
|
|
653
|
+
"""
|
|
654
|
+
Get all images positioned at or near a specific cell.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
cell_ref: Cell reference (e.g., "A1", "B2")
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
List[Image]: Images at the specified position
|
|
661
|
+
|
|
662
|
+
Example:
|
|
663
|
+
images = ws.get_images_at('B2')
|
|
664
|
+
"""
|
|
665
|
+
return self._images.get_by_position(cell_ref)
|
|
666
|
+
|
|
667
|
+
def clear_images(self):
|
|
668
|
+
"""Remove all images from the worksheet."""
|
|
669
|
+
self._images.clear()
|
|
670
|
+
|