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.
Files changed (53) hide show
  1. aspose/__init__.py +14 -0
  2. aspose/cells/__init__.py +31 -0
  3. aspose/cells/cell.py +350 -0
  4. aspose/cells/constants.py +44 -0
  5. aspose/cells/converters/__init__.py +13 -0
  6. aspose/cells/converters/csv_converter.py +55 -0
  7. aspose/cells/converters/json_converter.py +46 -0
  8. aspose/cells/converters/markdown_converter.py +453 -0
  9. aspose/cells/drawing/__init__.py +17 -0
  10. aspose/cells/drawing/anchor.py +172 -0
  11. aspose/cells/drawing/collection.py +233 -0
  12. aspose/cells/drawing/image.py +338 -0
  13. aspose/cells/formats.py +80 -0
  14. aspose/cells/formula/__init__.py +10 -0
  15. aspose/cells/formula/evaluator.py +360 -0
  16. aspose/cells/formula/functions.py +433 -0
  17. aspose/cells/formula/tokenizer.py +340 -0
  18. aspose/cells/io/__init__.py +27 -0
  19. aspose/cells/io/csv/__init__.py +8 -0
  20. aspose/cells/io/csv/reader.py +88 -0
  21. aspose/cells/io/csv/writer.py +98 -0
  22. aspose/cells/io/factory.py +138 -0
  23. aspose/cells/io/interfaces.py +48 -0
  24. aspose/cells/io/json/__init__.py +8 -0
  25. aspose/cells/io/json/reader.py +126 -0
  26. aspose/cells/io/json/writer.py +119 -0
  27. aspose/cells/io/md/__init__.py +8 -0
  28. aspose/cells/io/md/reader.py +161 -0
  29. aspose/cells/io/md/writer.py +334 -0
  30. aspose/cells/io/models.py +64 -0
  31. aspose/cells/io/xlsx/__init__.py +9 -0
  32. aspose/cells/io/xlsx/constants.py +312 -0
  33. aspose/cells/io/xlsx/image_writer.py +311 -0
  34. aspose/cells/io/xlsx/reader.py +284 -0
  35. aspose/cells/io/xlsx/writer.py +931 -0
  36. aspose/cells/plugins/__init__.py +6 -0
  37. aspose/cells/plugins/docling_backend/__init__.py +7 -0
  38. aspose/cells/plugins/docling_backend/backend.py +535 -0
  39. aspose/cells/plugins/markitdown_plugin/__init__.py +15 -0
  40. aspose/cells/plugins/markitdown_plugin/plugin.py +128 -0
  41. aspose/cells/range.py +210 -0
  42. aspose/cells/style.py +287 -0
  43. aspose/cells/utils/__init__.py +54 -0
  44. aspose/cells/utils/coordinates.py +68 -0
  45. aspose/cells/utils/exceptions.py +43 -0
  46. aspose/cells/utils/validation.py +102 -0
  47. aspose/cells/workbook.py +352 -0
  48. aspose/cells/worksheet.py +670 -0
  49. aspose_cells_foss-25.12.1.dist-info/METADATA +189 -0
  50. aspose_cells_foss-25.12.1.dist-info/RECORD +53 -0
  51. aspose_cells_foss-25.12.1.dist-info/WHEEL +5 -0
  52. aspose_cells_foss-25.12.1.dist-info/entry_points.txt +2 -0
  53. 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
+