Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/comments.py: 13%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1###############################################################################
2#
3# Comments - A class for writing the Excel XLSX Worksheet file.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6#
7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
8#
10from typing import Dict, List, Optional, Union
12from xlsxwriter import xmlwriter
13from xlsxwriter.color import Color
14from xlsxwriter.utility import (
15 _preserve_whitespace,
16 xl_cell_to_rowcol,
17 xl_rowcol_to_cell,
18)
21###########################################################################
22#
23# A comment type class.
24#
25###########################################################################
26class CommentType:
27 """
28 A class to represent a comment in an Excel worksheet.
30 """
32 def __init__(
33 self,
34 row: int,
35 col: int,
36 text: str,
37 options: Optional[Dict[str, Union[str, int, float]]] = None,
38 ) -> None:
39 """
40 Initialize a Comment instance.
42 Args:
43 row (int): The row number of the comment.
44 col (int): The column number of the comment.
45 text (str): The text of the comment.
46 options (dict): Additional options for the comment.
47 """
48 self.row: int = row
49 self.col: int = col
50 self.text: str = text
52 self.author: Optional[str] = None
53 self.color: Color = Color("#ffffe1")
55 self.start_row: int = 0
56 self.start_col: int = 0
58 self.is_visible: Optional[bool] = None
60 self.width: float = 128
61 self.height: float = 74
63 self.x_scale: float = 1
64 self.y_scale: float = 1
65 self.x_offset: int = 0
66 self.y_offset: int = 0
68 self.font_size: float = 8
69 self.font_name: str = "Tahoma"
70 self.font_family: int = 2
72 self.vertices: List[Union[int, float]] = []
74 # Set the default start cell and offsets for the comment.
75 self.set_offsets(self.row, self.col)
77 # Set any user supplied options.
78 self._set_user_options(options)
80 def _set_user_options(
81 self, options: Optional[Dict[str, Union[str, int, float]]] = None
82 ) -> None:
83 """
84 This method handles the additional optional parameters to
85 ``write_comment()``.
86 """
87 if options is None:
88 return
90 # Overwrite the defaults with any user supplied values. Incorrect or
91 # misspelled parameters are silently ignored.
92 width = options.get("width")
93 if width and isinstance(width, (int, float)):
94 self.width = width
96 height = options.get("height")
97 if height and isinstance(height, (int, float)):
98 self.height = height
100 x_offset = options.get("x_offset")
101 if x_offset and isinstance(x_offset, int):
102 self.x_offset = x_offset
104 y_offset = options.get("y_offset")
105 if y_offset and isinstance(y_offset, int):
106 self.y_offset = y_offset
108 start_col = options.get("start_col")
109 if start_col and isinstance(start_col, int):
110 self.start_col = start_col
112 start_row = options.get("start_row")
113 if start_row and isinstance(start_row, int):
114 self.start_row = start_row
116 font_size = options.get("font_size")
117 if font_size and isinstance(font_size, (int, float)):
118 self.font_size = font_size
120 font_name = options.get("font_name")
121 if font_name and isinstance(font_name, str):
122 self.font_name = font_name
124 font_family = options.get("font_family")
125 if font_family and isinstance(font_family, int):
126 self.font_family = font_family
128 author = options.get("author")
129 if author and isinstance(author, str):
130 self.author = author
132 visible = options.get("visible")
133 if visible is not None and isinstance(visible, bool):
134 self.is_visible = visible
136 if options.get("color"):
137 # Set the comment background color.
138 self.color = Color._from_value(options["color"])
140 # Convert a cell reference to a row and column.
141 start_cell = options.get("start_cell")
142 if start_cell and isinstance(start_cell, str):
143 (start_row, start_col) = xl_cell_to_rowcol(start_cell)
144 self.start_row = start_row
145 self.start_col = start_col
147 # Scale the size of the comment box if required.
148 x_scale = options.get("x_scale")
149 if x_scale and isinstance(x_scale, (int, float)):
150 self.width = self.width * x_scale
152 y_scale = options.get("y_scale")
153 if y_scale and isinstance(y_scale, (int, float)):
154 self.height = self.height * y_scale
156 # Round the dimensions to the nearest pixel.
157 self.width = int(0.5 + self.width)
158 self.height = int(0.5 + self.height)
160 def set_offsets(self, row: int, col: int) -> None:
161 """
162 Set the default start cell and offsets for the comment. These are
163 generally a fixed offset relative to the parent cell. However there are
164 some edge cases for cells at the, well, edges.
165 """
166 row_max = 1048576
167 col_max = 16384
169 if self.row == 0:
170 self.y_offset = 2
171 self.start_row = 0
172 elif self.row == row_max - 3:
173 self.y_offset = 16
174 self.start_row = row_max - 7
175 elif self.row == row_max - 2:
176 self.y_offset = 16
177 self.start_row = row_max - 6
178 elif self.row == row_max - 1:
179 self.y_offset = 14
180 self.start_row = row_max - 5
181 else:
182 self.y_offset = 10
183 self.start_row = row - 1
185 if self.col == col_max - 3:
186 self.x_offset = 49
187 self.start_col = col_max - 6
188 elif self.col == col_max - 2:
189 self.x_offset = 49
190 self.start_col = col_max - 5
191 elif self.col == col_max - 1:
192 self.x_offset = 49
193 self.start_col = col_max - 4
194 else:
195 self.x_offset = 15
196 self.start_col = col + 1
199###########################################################################
200#
201# The file writer class for the Excel XLSX Comments file.
202#
203###########################################################################
204class Comments(xmlwriter.XMLwriter):
205 """
206 A class for writing the Excel XLSX Comments file.
209 """
211 ###########################################################################
212 #
213 # Public API.
214 #
215 ###########################################################################
217 def __init__(self) -> None:
218 """
219 Constructor.
221 """
223 super().__init__()
224 self.author_ids = {}
226 ###########################################################################
227 #
228 # Private API.
229 #
230 ###########################################################################
232 def _assemble_xml_file(
233 self, comments_data: Optional[List[CommentType]] = None
234 ) -> None:
235 # Assemble and write the XML file.
237 if comments_data is None:
238 comments_data = []
240 # Write the XML declaration.
241 self._xml_declaration()
243 # Write the comments element.
244 self._write_comments()
246 # Write the authors element.
247 self._write_authors(comments_data)
249 # Write the commentList element.
250 self._write_comment_list(comments_data)
252 self._xml_end_tag("comments")
254 # Close the file.
255 self._xml_close()
257 ###########################################################################
258 #
259 # XML methods.
260 #
261 ###########################################################################
263 def _write_comments(self) -> None:
264 # Write the <comments> element.
265 xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
267 attributes = [("xmlns", xmlns)]
269 self._xml_start_tag("comments", attributes)
271 def _write_authors(self, comment_data: List[CommentType]) -> None:
272 # Write the <authors> element.
273 author_count = 0
275 self._xml_start_tag("authors")
277 for comment in comment_data:
278 author = comment.author
280 if author is not None and author not in self.author_ids:
281 # Store the author id.
282 self.author_ids[author] = author_count
283 author_count += 1
285 # Write the author element.
286 self._write_author(author)
288 self._xml_end_tag("authors")
290 def _write_author(self, data: str) -> None:
291 # Write the <author> element.
292 self._xml_data_element("author", data)
294 def _write_comment_list(self, comment_data: List[CommentType]) -> None:
295 # Write the <commentList> element.
296 self._xml_start_tag("commentList")
298 for comment in comment_data:
299 # Look up the author id.
300 author_id = -1
301 if comment.author is not None:
302 author_id = self.author_ids[comment.author]
304 # Write the comment element.
305 self._write_comment(comment, author_id)
307 self._xml_end_tag("commentList")
309 def _write_comment(self, comment: CommentType, author_id: int) -> None:
310 # Write the <comment> element.
311 ref = xl_rowcol_to_cell(comment.row, comment.col)
313 attributes = [("ref", ref)]
315 if author_id != -1:
316 attributes.append(("authorId", f"{author_id}"))
318 self._xml_start_tag("comment", attributes)
320 # Write the text element.
321 self._write_text(comment)
323 self._xml_end_tag("comment")
325 def _write_text(self, comment: CommentType) -> None:
326 # Write the <text> element.
327 self._xml_start_tag("text")
329 # Write the text r element.
330 self._write_text_r(comment)
332 self._xml_end_tag("text")
334 def _write_text_r(self, comment: CommentType) -> None:
335 # Write the <r> element.
336 self._xml_start_tag("r")
338 # Write the rPr element.
339 self._write_r_pr(comment)
341 # Write the text r element.
342 self._write_text_t(comment.text)
344 self._xml_end_tag("r")
346 def _write_text_t(self, text: str) -> None:
347 # Write the text <t> element.
348 attributes = []
350 if _preserve_whitespace(text):
351 attributes.append(("xml:space", "preserve"))
353 self._xml_data_element("t", text, attributes)
355 def _write_r_pr(self, comment: CommentType) -> None:
356 # Write the <rPr> element.
357 self._xml_start_tag("rPr")
359 # Write the sz element.
360 self._write_sz(comment.font_size)
362 # Write the color element.
363 self._write_color()
365 # Write the rFont element.
366 self._write_r_font(comment.font_name)
368 # Write the family element.
369 self._write_family(comment.font_family)
371 self._xml_end_tag("rPr")
373 def _write_sz(self, font_size: float) -> None:
374 # Write the <sz> element.
375 attributes = [("val", font_size)]
377 self._xml_empty_tag("sz", attributes)
379 def _write_color(self) -> None:
380 # Write the <color> element.
381 attributes = [("indexed", 81)]
383 self._xml_empty_tag("color", attributes)
385 def _write_r_font(self, font_name: str) -> None:
386 # Write the <rFont> element.
387 attributes = [("val", font_name)]
389 self._xml_empty_tag("rFont", attributes)
391 def _write_family(self, font_family: int) -> None:
392 # Write the <family> element.
393 attributes = [("val", font_family)]
395 self._xml_empty_tag("family", attributes)