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.
- aspose_cells/__init__.py +88 -0
- aspose_cells/auto_filter.py +527 -0
- aspose_cells/cell.py +483 -0
- aspose_cells/cell_value_handler.py +319 -0
- aspose_cells/cells.py +779 -0
- aspose_cells/cfb_handler.py +445 -0
- aspose_cells/cfb_writer.py +659 -0
- aspose_cells/cfb_writer_minimal.py +337 -0
- aspose_cells/comment_xml.py +475 -0
- aspose_cells/conditional_format.py +1185 -0
- aspose_cells/csv_handler.py +690 -0
- aspose_cells/data_validation.py +911 -0
- aspose_cells/document_properties.py +356 -0
- aspose_cells/encryption_crypto.py +247 -0
- aspose_cells/encryption_params.py +138 -0
- aspose_cells/hyperlink.py +372 -0
- aspose_cells/json_handler.py +185 -0
- aspose_cells/markdown_handler.py +583 -0
- aspose_cells/shared_strings.py +101 -0
- aspose_cells/style.py +841 -0
- aspose_cells/workbook.py +499 -0
- aspose_cells/workbook_hash_password.py +68 -0
- aspose_cells/workbook_properties.py +712 -0
- aspose_cells/worksheet.py +570 -0
- aspose_cells/worksheet_properties.py +1239 -0
- aspose_cells/xlsx_encryptor.py +403 -0
- aspose_cells/xml_autofilter_loader.py +195 -0
- aspose_cells/xml_autofilter_saver.py +173 -0
- aspose_cells/xml_conditional_format_loader.py +215 -0
- aspose_cells/xml_conditional_format_saver.py +351 -0
- aspose_cells/xml_datavalidation_loader.py +239 -0
- aspose_cells/xml_datavalidation_saver.py +245 -0
- aspose_cells/xml_hyperlink_handler.py +323 -0
- aspose_cells/xml_loader.py +986 -0
- aspose_cells/xml_properties_loader.py +512 -0
- aspose_cells/xml_properties_saver.py +607 -0
- aspose_cells/xml_saver.py +1306 -0
- aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
- aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
- {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
- aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
- aspose/__init__.py +0 -14
- aspose/cells/__init__.py +0 -31
- aspose/cells/cell.py +0 -350
- aspose/cells/constants.py +0 -44
- aspose/cells/converters/__init__.py +0 -13
- aspose/cells/converters/csv_converter.py +0 -55
- aspose/cells/converters/json_converter.py +0 -46
- aspose/cells/converters/markdown_converter.py +0 -453
- aspose/cells/drawing/__init__.py +0 -17
- aspose/cells/drawing/anchor.py +0 -172
- aspose/cells/drawing/collection.py +0 -233
- aspose/cells/drawing/image.py +0 -338
- aspose/cells/formats.py +0 -80
- aspose/cells/formula/__init__.py +0 -10
- aspose/cells/formula/evaluator.py +0 -360
- aspose/cells/formula/functions.py +0 -433
- aspose/cells/formula/tokenizer.py +0 -340
- aspose/cells/io/__init__.py +0 -27
- aspose/cells/io/csv/__init__.py +0 -8
- aspose/cells/io/csv/reader.py +0 -88
- aspose/cells/io/csv/writer.py +0 -98
- aspose/cells/io/factory.py +0 -138
- aspose/cells/io/interfaces.py +0 -48
- aspose/cells/io/json/__init__.py +0 -8
- aspose/cells/io/json/reader.py +0 -126
- aspose/cells/io/json/writer.py +0 -119
- aspose/cells/io/md/__init__.py +0 -8
- aspose/cells/io/md/reader.py +0 -161
- aspose/cells/io/md/writer.py +0 -334
- aspose/cells/io/models.py +0 -64
- aspose/cells/io/xlsx/__init__.py +0 -9
- aspose/cells/io/xlsx/constants.py +0 -312
- aspose/cells/io/xlsx/image_writer.py +0 -311
- aspose/cells/io/xlsx/reader.py +0 -284
- aspose/cells/io/xlsx/writer.py +0 -931
- aspose/cells/plugins/__init__.py +0 -6
- aspose/cells/plugins/docling_backend/__init__.py +0 -7
- aspose/cells/plugins/docling_backend/backend.py +0 -535
- aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
- aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
- aspose/cells/range.py +0 -210
- aspose/cells/style.py +0 -287
- aspose/cells/utils/__init__.py +0 -54
- aspose/cells/utils/coordinates.py +0 -68
- aspose/cells/utils/exceptions.py +0 -43
- aspose/cells/utils/validation.py +0 -102
- aspose/cells/workbook.py +0 -352
- aspose/cells/worksheet.py +0 -670
- aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
- aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
- 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('<', '<')
|
|
53
|
+
text = text.replace('>', '>')
|
|
54
|
+
text = text.replace('"', '"')
|
|
55
|
+
text = text.replace("'", ''')
|
|
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)
|