1# Copyright (c) 2010-2024 openpyxl
2
3from openpyxl.xml.functions import (
4 Element,
5 SubElement,
6 tostring,
7)
8
9from openpyxl.utils import coordinate_to_tuple
10
11vmlns = "urn:schemas-microsoft-com:vml"
12officens = "urn:schemas-microsoft-com:office:office"
13excelns = "urn:schemas-microsoft-com:office:excel"
14
15
16class ShapeWriter:
17 """
18 Create VML for comments
19 """
20
21 vml = None
22 vml_path = None
23
24
25 def __init__(self, comments):
26 self.comments = comments
27
28
29 def add_comment_shapetype(self, root):
30 shape_layout = SubElement(root, "{%s}shapelayout" % officens,
31 {"{%s}ext" % vmlns: "edit"})
32 SubElement(shape_layout,
33 "{%s}idmap" % officens,
34 {"{%s}ext" % vmlns: "edit", "data": "1"})
35 shape_type = SubElement(root,
36 "{%s}shapetype" % vmlns,
37 {"id": "_x0000_t202",
38 "coordsize": "21600,21600",
39 "{%s}spt" % officens: "202",
40 "path": "m,l,21600r21600,l21600,xe"})
41 SubElement(shape_type, "{%s}stroke" % vmlns, {"joinstyle": "miter"})
42 SubElement(shape_type,
43 "{%s}path" % vmlns,
44 {"gradientshapeok": "t",
45 "{%s}connecttype" % officens: "rect"})
46
47
48 def add_comment_shape(self, root, idx, coord, height, width):
49 row, col = coordinate_to_tuple(coord)
50 row -= 1
51 col -= 1
52 shape = _shape_factory(row, col, height, width)
53
54 shape.set('id', "_x0000_s%04d" % idx)
55 root.append(shape)
56
57
58 def write(self, root):
59
60 if not hasattr(root, "findall"):
61 root = Element("xml")
62
63 # Remove any existing comment shapes
64 comments = root.findall("{%s}shape[@type='#_x0000_t202']" % vmlns)
65 for c in comments:
66 root.remove(c)
67
68 # check whether comments shape type already exists
69 shape_types = root.find("{%s}shapetype[@id='_x0000_t202']" % vmlns)
70 if shape_types is None:
71 self.add_comment_shapetype(root)
72
73 for idx, (coord, comment) in enumerate(self.comments, 1026):
74 self.add_comment_shape(root, idx, coord, comment.height, comment.width)
75
76 return tostring(root)
77
78
79def _shape_factory(row, column, height, width):
80 style = ("position:absolute; "
81 "margin-left:59.25pt;"
82 "margin-top:1.5pt;"
83 "width:{width}px;"
84 "height:{height}px;"
85 "z-index:1;"
86 "visibility:hidden").format(height=height,
87 width=width)
88 attrs = {
89 "type": "#_x0000_t202",
90 "style": style,
91 "fillcolor": "#ffffe1",
92 "{%s}insetmode" % officens: "auto"
93 }
94 shape = Element("{%s}shape" % vmlns, attrs)
95
96 SubElement(shape, "{%s}fill" % vmlns,
97 {"color2": "#ffffe1"})
98 SubElement(shape, "{%s}shadow" % vmlns,
99 {"color": "black", "obscured": "t"})
100 SubElement(shape, "{%s}path" % vmlns,
101 {"{%s}connecttype" % officens: "none"})
102 textbox = SubElement(shape, "{%s}textbox" % vmlns,
103 {"style": "mso-direction-alt:auto"})
104 SubElement(textbox, "div", {"style": "text-align:left"})
105 client_data = SubElement(shape, "{%s}ClientData" % excelns,
106 {"ObjectType": "Note"})
107 SubElement(client_data, "{%s}MoveWithCells" % excelns)
108 SubElement(client_data, "{%s}SizeWithCells" % excelns)
109 SubElement(client_data, "{%s}AutoFill" % excelns).text = "False"
110 SubElement(client_data, "{%s}Row" % excelns).text = str(row)
111 SubElement(client_data, "{%s}Column" % excelns).text = str(column)
112 return shape