aspose-cells-foss 25.12.1__py3-none-any.whl → 26.2.2__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 (93) hide show
  1. aspose_cells/__init__.py +88 -0
  2. aspose_cells/auto_filter.py +527 -0
  3. aspose_cells/cell.py +483 -0
  4. aspose_cells/cell_value_handler.py +319 -0
  5. aspose_cells/cells.py +779 -0
  6. aspose_cells/cfb_handler.py +445 -0
  7. aspose_cells/cfb_writer.py +659 -0
  8. aspose_cells/cfb_writer_minimal.py +337 -0
  9. aspose_cells/comment_xml.py +475 -0
  10. aspose_cells/conditional_format.py +1185 -0
  11. aspose_cells/csv_handler.py +690 -0
  12. aspose_cells/data_validation.py +911 -0
  13. aspose_cells/document_properties.py +356 -0
  14. aspose_cells/encryption_crypto.py +247 -0
  15. aspose_cells/encryption_params.py +138 -0
  16. aspose_cells/hyperlink.py +372 -0
  17. aspose_cells/json_handler.py +185 -0
  18. aspose_cells/markdown_handler.py +583 -0
  19. aspose_cells/shared_strings.py +101 -0
  20. aspose_cells/style.py +841 -0
  21. aspose_cells/workbook.py +499 -0
  22. aspose_cells/workbook_hash_password.py +68 -0
  23. aspose_cells/workbook_properties.py +712 -0
  24. aspose_cells/worksheet.py +570 -0
  25. aspose_cells/worksheet_properties.py +1239 -0
  26. aspose_cells/xlsx_encryptor.py +403 -0
  27. aspose_cells/xml_autofilter_loader.py +195 -0
  28. aspose_cells/xml_autofilter_saver.py +173 -0
  29. aspose_cells/xml_conditional_format_loader.py +215 -0
  30. aspose_cells/xml_conditional_format_saver.py +351 -0
  31. aspose_cells/xml_datavalidation_loader.py +239 -0
  32. aspose_cells/xml_datavalidation_saver.py +245 -0
  33. aspose_cells/xml_hyperlink_handler.py +323 -0
  34. aspose_cells/xml_loader.py +986 -0
  35. aspose_cells/xml_properties_loader.py +512 -0
  36. aspose_cells/xml_properties_saver.py +607 -0
  37. aspose_cells/xml_saver.py +1306 -0
  38. aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
  39. aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
  40. {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
  41. aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
  42. aspose/__init__.py +0 -14
  43. aspose/cells/__init__.py +0 -31
  44. aspose/cells/cell.py +0 -350
  45. aspose/cells/constants.py +0 -44
  46. aspose/cells/converters/__init__.py +0 -13
  47. aspose/cells/converters/csv_converter.py +0 -55
  48. aspose/cells/converters/json_converter.py +0 -46
  49. aspose/cells/converters/markdown_converter.py +0 -453
  50. aspose/cells/drawing/__init__.py +0 -17
  51. aspose/cells/drawing/anchor.py +0 -172
  52. aspose/cells/drawing/collection.py +0 -233
  53. aspose/cells/drawing/image.py +0 -338
  54. aspose/cells/formats.py +0 -80
  55. aspose/cells/formula/__init__.py +0 -10
  56. aspose/cells/formula/evaluator.py +0 -360
  57. aspose/cells/formula/functions.py +0 -433
  58. aspose/cells/formula/tokenizer.py +0 -340
  59. aspose/cells/io/__init__.py +0 -27
  60. aspose/cells/io/csv/__init__.py +0 -8
  61. aspose/cells/io/csv/reader.py +0 -88
  62. aspose/cells/io/csv/writer.py +0 -98
  63. aspose/cells/io/factory.py +0 -138
  64. aspose/cells/io/interfaces.py +0 -48
  65. aspose/cells/io/json/__init__.py +0 -8
  66. aspose/cells/io/json/reader.py +0 -126
  67. aspose/cells/io/json/writer.py +0 -119
  68. aspose/cells/io/md/__init__.py +0 -8
  69. aspose/cells/io/md/reader.py +0 -161
  70. aspose/cells/io/md/writer.py +0 -334
  71. aspose/cells/io/models.py +0 -64
  72. aspose/cells/io/xlsx/__init__.py +0 -9
  73. aspose/cells/io/xlsx/constants.py +0 -312
  74. aspose/cells/io/xlsx/image_writer.py +0 -311
  75. aspose/cells/io/xlsx/reader.py +0 -284
  76. aspose/cells/io/xlsx/writer.py +0 -931
  77. aspose/cells/plugins/__init__.py +0 -6
  78. aspose/cells/plugins/docling_backend/__init__.py +0 -7
  79. aspose/cells/plugins/docling_backend/backend.py +0 -535
  80. aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
  81. aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
  82. aspose/cells/range.py +0 -210
  83. aspose/cells/style.py +0 -287
  84. aspose/cells/utils/__init__.py +0 -54
  85. aspose/cells/utils/coordinates.py +0 -68
  86. aspose/cells/utils/exceptions.py +0 -43
  87. aspose/cells/utils/validation.py +0 -102
  88. aspose/cells/workbook.py +0 -352
  89. aspose/cells/worksheet.py +0 -670
  90. aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
  91. aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
  92. aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
  93. aspose_cells_foss-25.12.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,475 @@
1
+ """
2
+ Aspose.Cells for Python - Comment XML Module
3
+
4
+ This module handles reading and writing comment data to/from XML format
5
+ according to ECMA-376 standards.
6
+
7
+ Comment data is stored in:
8
+ - xl/comments{n}.xml - Comment text and authors
9
+ - xl/drawings/vmlDrawing{n}.vml - Comment positioning and sizing (VML format)
10
+ """
11
+
12
+ import re
13
+ import xml.etree.ElementTree as ET
14
+
15
+
16
+ # Default comment dimensions (Excel standard)
17
+ DEFAULT_COMMENT_WIDTH = 96 # points
18
+ DEFAULT_COMMENT_HEIGHT = 55.5 # points
19
+
20
+ # Standard Excel dimensions for anchor calculations
21
+ AVG_COL_WIDTH_PT = 48 # Standard column width in points
22
+ AVG_ROW_HEIGHT_PT = 15 # Standard row height in points
23
+
24
+
25
+ class CommentXMLWriter:
26
+ """
27
+ Handles writing comment data to XML format.
28
+
29
+ Writes both comments XML and VML drawing files for proper
30
+ Excel compatibility according to ECMA-376.
31
+ """
32
+
33
+ def __init__(self):
34
+ """Initialize the CommentXMLWriter."""
35
+ pass
36
+
37
+ @staticmethod
38
+ def escape_xml(text):
39
+ """
40
+ Escapes special XML characters in text.
41
+
42
+ Args:
43
+ text: The text to escape.
44
+
45
+ Returns:
46
+ str: The escaped text.
47
+ """
48
+ if text is None:
49
+ return ''
50
+ text = str(text)
51
+ text = text.replace('&', '&')
52
+ text = text.replace('<', '&lt;')
53
+ text = text.replace('>', '&gt;')
54
+ text = text.replace('"', '&quot;')
55
+ text = text.replace("'", '&apos;')
56
+ return text
57
+
58
+ @staticmethod
59
+ def worksheet_has_comments(worksheet):
60
+ """
61
+ Checks if a worksheet has any comments.
62
+
63
+ Args:
64
+ worksheet: The worksheet to check.
65
+
66
+ Returns:
67
+ bool: True if the worksheet has comments, False otherwise.
68
+ """
69
+ for cell in worksheet.cells._cells.values():
70
+ if cell.has_comment():
71
+ return True
72
+ return False
73
+
74
+ @staticmethod
75
+ def cell_reference_sort_key(ref):
76
+ """
77
+ Converts a cell reference to a (row, col) tuple for sorting.
78
+
79
+ Args:
80
+ ref: Cell reference string (e.g., 'A1', 'BC23').
81
+
82
+ Returns:
83
+ tuple: (row, col) where both are 1-based integers.
84
+ """
85
+ col_str = ''
86
+ row_str = ''
87
+ for char in ref:
88
+ if char.isalpha():
89
+ col_str += char
90
+ else:
91
+ row_str += char
92
+
93
+ # Convert column letters to number
94
+ col = 0
95
+ for char in col_str.upper():
96
+ col = col * 26 + (ord(char) - ord('A') + 1)
97
+
98
+ row = int(row_str) if row_str else 0
99
+ return (row, col)
100
+
101
+ def write_comments_xml(self, zipf, worksheet, sheet_num):
102
+ """
103
+ Writes xl/comments{sheet_num}.xml file for a worksheet.
104
+
105
+ According to ECMA-376 Part 1, Section 18.7.3, comments are stored
106
+ in a separate XML file with authors and comment text.
107
+
108
+ ECMA-376 compliant format includes:
109
+ - Rich text format with <r> (run) elements
110
+ - Author name prepended to comment text with bold formatting
111
+ - Font properties (size, color, font family)
112
+ - shapeId attribute for each comment
113
+
114
+ Args:
115
+ zipf: The ZIP file object to write to.
116
+ worksheet: The worksheet object.
117
+ sheet_num: The worksheet number (1-based).
118
+ """
119
+ if not self.worksheet_has_comments(worksheet):
120
+ return
121
+
122
+ # Collect all unique authors and comments
123
+ authors = set()
124
+ comments_data = []
125
+
126
+ for ref, cell in worksheet.cells._cells.items():
127
+ if cell.has_comment():
128
+ comment = cell.get_comment()
129
+ author = comment.get('author', '')
130
+ text = comment.get('text', '')
131
+
132
+ if author not in authors:
133
+ authors.add(author)
134
+
135
+ comments_data.append({
136
+ 'ref': ref,
137
+ 'author': author,
138
+ 'text': text
139
+ })
140
+
141
+ authors_list = list(authors)
142
+
143
+ # Build comments XML with ECMA-376 compliant rich text format
144
+ content = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
145
+ content += '<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
146
+ content += 'xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" '
147
+ content += 'mc:Ignorable="xr" '
148
+ content += 'xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision">\n'
149
+
150
+ # Write authors section (no count attribute per ECMA-376)
151
+ content += ' <authors>\n'
152
+ for author in authors_list:
153
+ escaped_author = self.escape_xml(author)
154
+ content += f' <author>{escaped_author}</author>\n'
155
+ content += ' </authors>\n'
156
+
157
+ # Write comment list with rich text format (no count attribute per ECMA-376)
158
+ content += ' <commentList>\n'
159
+ for comment_data in comments_data:
160
+ author_idx = authors_list.index(comment_data['author'])
161
+ author_name = comment_data['author']
162
+ comment_text = comment_data['text']
163
+
164
+ # shapeId is always 0 per Excel's implementation
165
+ content += f' <comment ref="{comment_data["ref"]}" authorId="{author_idx}" shapeId="0">\n'
166
+ content += ' <text>\n'
167
+
168
+ # Author name run (bold with formatting)
169
+ content += ' <r>\n'
170
+ content += ' <rPr>\n'
171
+ content += ' <b/>\n'
172
+ content += ' <sz val="9"/>\n'
173
+ content += ' <color indexed="81"/>\n'
174
+ content += ' <rFont val="Tahoma"/>\n'
175
+ content += ' <family val="2"/>\n'
176
+ content += ' </rPr>\n'
177
+ content += f' <t>{self.escape_xml(author_name)}:</t>\n'
178
+ content += ' </r>\n'
179
+
180
+ # Comment text run
181
+ content += ' <r>\n'
182
+ content += ' <rPr>\n'
183
+ content += ' <sz val="9"/>\n'
184
+ content += ' <color indexed="81"/>\n'
185
+ content += ' <rFont val="Tahoma"/>\n'
186
+ content += ' <family val="2"/>\n'
187
+ content += ' </rPr>\n'
188
+ content += f' <t xml:space="preserve">{self.escape_xml(comment_text)}</t>\n'
189
+ content += ' </r>\n'
190
+
191
+ content += ' </text>\n'
192
+ content += ' </comment>\n'
193
+ content += ' </commentList>\n'
194
+
195
+ content += '</comments>\n'
196
+ zipf.writestr(f'xl/comments{sheet_num}.xml', content)
197
+
198
+ def write_vml_drawing_xml(self, zipf, worksheet, sheet_num):
199
+ """
200
+ Writes VML drawing XML for comment positioning.
201
+
202
+ According to ECMA-376 Part 1, Section 18.3.1.43, legacy drawings
203
+ are used for backward compatibility with older Excel versions and
204
+ contain shape information for comment positioning.
205
+
206
+ The Anchor element defines cell-relative position and size using 8 values:
207
+ colStart, xOffset, rowStart, yOffset, colEnd, xOffsetEnd, rowEnd, yOffsetEnd
208
+
209
+ Args:
210
+ zipf: The ZIP file object to write to.
211
+ worksheet: The worksheet object.
212
+ sheet_num: The worksheet number (1-based).
213
+ """
214
+ if not self.worksheet_has_comments(worksheet):
215
+ return
216
+
217
+ content = '<xml xmlns:v="urn:schemas-microsoft-com:vml"\n'
218
+ content += ' xmlns:o="urn:schemas-microsoft-com:office:office"\n'
219
+ content += ' xmlns:x="urn:schemas-microsoft-com:office:excel">\n'
220
+ content += ' <o:shapelayout v:ext="edit">\n'
221
+ content += ' <o:idmap v:ext="edit" data="1"/>\n'
222
+ content += ' </o:shapelayout>\n'
223
+ content += ' <v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202"\n'
224
+ content += ' path="m,l,21600r21600,l21600,xe">\n'
225
+ content += ' <v:stroke joinstyle="miter"/>\n'
226
+ content += ' <v:path gradientshapeok="t" o:connecttype="rect"/>\n'
227
+ content += ' </v:shapetype>\n'
228
+
229
+ # Add shapes for each comment
230
+ shape_id = 1025
231
+ for ref, cell in worksheet.cells._cells.items():
232
+ if cell.has_comment():
233
+ row, col = self.cell_reference_sort_key(ref)
234
+ comment = cell.get_comment()
235
+
236
+ # Get comment size (default to Excel's default size if not specified)
237
+ width = comment.get('width')
238
+ height = comment.get('height')
239
+
240
+ # Use Excel defaults if not specified
241
+ if width is None:
242
+ width = DEFAULT_COMMENT_WIDTH
243
+ if height is None:
244
+ height = DEFAULT_COMMENT_HEIGHT
245
+
246
+ # Calculate anchor values from width/height
247
+ anchor = self._calculate_anchor(row, col, width, height)
248
+
249
+ content += f' <v:shape id="_x0000_s{shape_id}" type="#_x0000_t202"\n'
250
+ content += f' style="position:absolute;margin-left:59.25pt;margin-top:1.5pt;width:{width}pt;height:{height}pt;z-index:{shape_id-1024};visibility:hidden"\n'
251
+ content += ' fillcolor="infoBackground [80]" strokecolor="none [81]"\n'
252
+ content += ' o:insetmode="auto">\n'
253
+ content += ' <v:fill color2="infoBackground [80]"/>\n'
254
+ content += ' <v:shadow color="none [81]"/>\n'
255
+ content += ' <v:textbox>\n'
256
+ content += ' <div style="text-align:left"></div>\n'
257
+ content += ' </v:textbox>\n'
258
+ content += ' <x:ClientData ObjectType="Note">\n'
259
+ content += ' <x:MoveWithCells/>\n'
260
+ content += ' <x:SizeWithCells/>\n'
261
+ content += f' <x:Anchor>{anchor}</x:Anchor>\n'
262
+ content += f' <x:Row>{row-1}</x:Row>\n'
263
+ content += f' <x:Column>{col-1}</x:Column>\n'
264
+ content += ' </x:ClientData>\n'
265
+ content += ' </v:shape>\n'
266
+
267
+ shape_id += 1
268
+
269
+ content += '</xml>\n'
270
+ zipf.writestr(f'xl/drawings/vmlDrawing{sheet_num}.vml', content)
271
+
272
+ def _calculate_anchor(self, row, col, width, height):
273
+ """
274
+ Calculate anchor coordinates from width/height in points.
275
+
276
+ Anchor format: colStart, xOffset, rowStart, yOffset, colEnd, xOffsetEnd, rowEnd, yOffsetEnd
277
+
278
+ Args:
279
+ row: Cell row (1-based).
280
+ col: Cell column (1-based).
281
+ width: Comment width in points.
282
+ height: Comment height in points.
283
+
284
+ Returns:
285
+ str: Anchor string with 8 comma-separated values.
286
+ """
287
+ # Calculate how many columns/rows the comment spans
288
+ col_span = int(width / AVG_COL_WIDTH_PT)
289
+ row_span = int(height / AVG_ROW_HEIGHT_PT)
290
+
291
+ # Calculate offsets (remainder after full column/row spans)
292
+ # X offsets in 1/256ths of column width
293
+ x_offset_start = 12 # Small offset to right of cell
294
+ x_offset_end = int(((width % AVG_COL_WIDTH_PT) / AVG_COL_WIDTH_PT) * 256)
295
+
296
+ # Y offsets in points or 1/256ths of row height
297
+ y_offset_start = 4 # Small offset below row
298
+ y_offset_end = int(((height % AVG_ROW_HEIGHT_PT) / AVG_ROW_HEIGHT_PT) * 256)
299
+
300
+ # Position comment starting from cell position
301
+ # Excel typically positions comments 2 rows above and at the same column
302
+ anchor_col_start = col - 1 # 0-based
303
+ anchor_row_start = max(0, row - 3) # Start 2 rows above (0-based, so -3)
304
+ anchor_col_end = anchor_col_start + col_span
305
+ anchor_row_end = anchor_row_start + row_span
306
+
307
+ return f"{anchor_col_start}, {x_offset_start}, {anchor_row_start}, {y_offset_start}, {anchor_col_end}, {x_offset_end}, {anchor_row_end}, {y_offset_end}"
308
+
309
+
310
+ class CommentXMLReader:
311
+ """
312
+ Handles reading comment data from XML format.
313
+
314
+ Reads both comments XML and VML drawing files to restore
315
+ comment text, authors, and sizing information.
316
+ """
317
+
318
+ # XML namespaces for parsing
319
+ NS = {'main': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
320
+
321
+ def __init__(self):
322
+ """Initialize the CommentXMLReader."""
323
+ pass
324
+
325
+ def load_comments(self, zipf, worksheet, sheet_num):
326
+ """
327
+ Loads comments from the comments XML file for a worksheet.
328
+
329
+ According to ECMA-376 Part 1, Section 18.7.3, comments are stored in
330
+ xl/comments{i}.xml and contain author information and comment text.
331
+
332
+ ECMA-376 compliant format includes rich text with <r> (run) elements.
333
+ The author name is typically prepended to the comment text with bold formatting.
334
+
335
+ Args:
336
+ zipf: A ZipFile object containing the workbook data.
337
+ worksheet: The worksheet object to load comments into.
338
+ sheet_num: The worksheet number (1-based).
339
+ """
340
+ try:
341
+ comments_content = zipf.read(f'xl/comments{sheet_num}.xml')
342
+ comments_root = ET.fromstring(comments_content)
343
+
344
+ # Get authors list (ECMA-376: authors are stored separately)
345
+ authors = []
346
+ authors_elem = comments_root.find('.//main:authors', namespaces=self.NS)
347
+ if authors_elem is not None:
348
+ for author in authors_elem.findall('main:author', namespaces=self.NS):
349
+ authors.append(author.text if author.text else '')
350
+
351
+ # Load comments
352
+ comments_elem = comments_root.find('.//main:commentList', namespaces=self.NS)
353
+ if comments_elem is not None:
354
+ for comment in comments_elem.findall('main:comment', namespaces=self.NS):
355
+ # Get cell reference
356
+ cell_ref = comment.get('ref')
357
+
358
+ # Get author index
359
+ author_idx = int(comment.get('authorId', 0))
360
+ author = authors[author_idx] if author_idx < len(authors) else ''
361
+
362
+ # Get comment text from rich text runs
363
+ # ECMA-376: text can be in <t> elements within <r> (run) elements
364
+ comment_text = ''
365
+ text_elem = comment.find('.//main:text', namespaces=self.NS)
366
+ if text_elem is not None:
367
+ # Collect all text from <t> elements
368
+ for t in text_elem.findall('.//main:t', namespaces=self.NS):
369
+ if t.text:
370
+ comment_text += t.text
371
+
372
+ # Remove author prefix if present (e.g., "Author Name:")
373
+ # Excel formats comments with author name prepended
374
+ if author and comment_text.startswith(f'{author}:'):
375
+ comment_text = comment_text[len(author)+1:].lstrip()
376
+
377
+ # Create cell if it doesn't exist (comments can exist on empty cells)
378
+ if cell_ref not in worksheet.cells._cells:
379
+ from .cell import Cell
380
+ worksheet.cells._cells[cell_ref] = Cell(None, None)
381
+
382
+ # Set comment on the cell
383
+ cell = worksheet.cells._cells[cell_ref]
384
+ cell.set_comment(comment_text, author)
385
+
386
+ # Load VML drawing for comment sizes
387
+ self.load_vml_drawing(zipf, worksheet, sheet_num)
388
+ except KeyError:
389
+ # Comments file not found, skip
390
+ pass
391
+
392
+ def load_vml_drawing(self, zipf, worksheet, sheet_num):
393
+ """
394
+ Loads VML drawing for comment positioning and sizing.
395
+
396
+ Parses the Anchor element to extract comment size information
397
+ and associates it with the corresponding cells.
398
+
399
+ Args:
400
+ zipf: A ZipFile object containing the workbook data.
401
+ worksheet: The worksheet object.
402
+ sheet_num: The worksheet number (1-based).
403
+ """
404
+ try:
405
+ vml_content = zipf.read(f'xl/drawings/vmlDrawing{sheet_num}.vml').decode('utf-8')
406
+
407
+ # Split by v:shape tags to process each shape individually
408
+ # Match only actual comment shapes (with id="_x0000_s..."), not shapetype
409
+ shapes = re.findall(r'<v:shape[^>]*id="_x0000_s\d+"[^>]*>.*?</v:shape>', vml_content, re.DOTALL)
410
+
411
+ for shape in shapes:
412
+ # Extract anchor, row, and column
413
+ anchor_match = re.search(r'<x:Anchor>([^<]+)</x:Anchor>', shape)
414
+ row_match = re.search(r'<x:Row>(\d+)</x:Row>', shape)
415
+ col_match = re.search(r'<x:Column>(\d+)</x:Column>', shape)
416
+
417
+ if anchor_match and row_match and col_match:
418
+ anchor_str = anchor_match.group(1).strip()
419
+ row = int(row_match.group(1)) + 1 # VML uses 0-based indexing
420
+ col = int(col_match.group(1)) + 1
421
+
422
+ # Parse anchor values and calculate size
423
+ width, height = self._parse_anchor_to_size(anchor_str)
424
+
425
+ if width is not None and height is not None:
426
+ # Find the cell with this comment
427
+ from .cells import Cells
428
+ col_letter = Cells.column_letter_from_index(col)
429
+ cell_ref = f"{col_letter}{row}"
430
+
431
+ if cell_ref in worksheet.cells._cells:
432
+ cell = worksheet.cells._cells[cell_ref]
433
+ if cell.has_comment():
434
+ cell._comment['width'] = round(width, 1)
435
+ cell._comment['height'] = round(height, 1)
436
+
437
+ except (KeyError, Exception):
438
+ # VML drawing not found or parsing error, skip
439
+ pass
440
+
441
+ def _parse_anchor_to_size(self, anchor_str):
442
+ """
443
+ Parse anchor string and calculate width/height in points.
444
+
445
+ Anchor format: colStart, xOffset, rowStart, yOffset, colEnd, xOffsetEnd, rowEnd, yOffsetEnd
446
+
447
+ Args:
448
+ anchor_str: The anchor string with 8 comma-separated values.
449
+
450
+ Returns:
451
+ tuple: (width, height) in points, or (None, None) if parsing fails.
452
+ """
453
+ try:
454
+ anchor_values = [x.strip() for x in anchor_str.split(',')]
455
+ if len(anchor_values) == 8:
456
+ col_start = int(anchor_values[0])
457
+ col_end = int(anchor_values[4])
458
+ x_offset_end = int(anchor_values[5])
459
+ row_start = int(anchor_values[2])
460
+ row_end = int(anchor_values[6])
461
+ y_offset_end = int(anchor_values[7])
462
+
463
+ # Calculate column span and width
464
+ col_span = col_end - col_start
465
+ width = col_span * AVG_COL_WIDTH_PT + (x_offset_end / 256.0) * AVG_COL_WIDTH_PT
466
+
467
+ # Calculate row span and height
468
+ row_span = row_end - row_start
469
+ height = row_span * AVG_ROW_HEIGHT_PT + (y_offset_end / 256.0) * AVG_ROW_HEIGHT_PT
470
+
471
+ return (width, height)
472
+ except (ValueError, IndexError):
473
+ pass
474
+
475
+ return (None, None)