Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/shape.py: 11%

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

198 statements  

1############################################################################### 

2# 

3# Shape - A class for to represent Excel XLSX shape objects. 

4# 

5# SPDX-License-Identifier: BSD-2-Clause 

6# 

7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org 

8# 

9import copy 

10from warnings import warn 

11 

12from xlsxwriter.color import Color 

13 

14 

15class Shape: 

16 """ 

17 A class for to represent Excel XLSX shape objects. 

18 

19 

20 """ 

21 

22 ########################################################################### 

23 # 

24 # Public API. 

25 # 

26 ########################################################################### 

27 

28 def __init__(self, shape_type, name: str, options) -> None: 

29 """ 

30 Constructor. 

31 

32 """ 

33 super().__init__() 

34 self.name = name 

35 self.shape_type = shape_type 

36 self.connect = 0 

37 self.drawing = 0 

38 self.edit_as = "" 

39 self.id = 0 

40 self.text = "" 

41 self.textlink = "" 

42 self.stencil = 1 

43 self.element = -1 

44 self.start = None 

45 self.start_index = None 

46 self.end = None 

47 self.end_index = None 

48 self.adjustments = [] 

49 self.start_side = "" 

50 self.end_side = "" 

51 self.flip_h = 0 

52 self.flip_v = 0 

53 self.rotation = 0 

54 self.text_rotation = 0 

55 self.textbox = False 

56 

57 self.align = None 

58 self.fill = None 

59 self.font = None 

60 self.format = None 

61 self.line = None 

62 

63 self._set_options(options) 

64 

65 ########################################################################### 

66 # 

67 # Private API. 

68 # 

69 ########################################################################### 

70 

71 def _set_options(self, options) -> None: 

72 self.align = self._get_align_properties(options.get("align")) 

73 self.fill = self._get_fill_properties(options.get("fill")) 

74 self.font = self._get_font_properties(options.get("font")) 

75 self.gradient = self._get_gradient_properties(options.get("gradient")) 

76 self.line = self._get_line_properties(options) 

77 

78 self.text_rotation = options.get("text_rotation", 0) 

79 

80 self.textlink = options.get("textlink", "") 

81 if self.textlink.startswith("="): 

82 self.textlink = self.textlink.lstrip("=") 

83 

84 # Gradient fill overrides solid fill. 

85 if self.gradient: 

86 self.fill = None 

87 

88 ########################################################################### 

89 # 

90 # Static methods for processing chart/shape style properties. 

91 # 

92 ########################################################################### 

93 

94 @staticmethod 

95 def _get_line_properties(options: dict) -> dict: 

96 # Convert user line properties to the structure required internally. 

97 if not options.get("line") and not options.get("border"): 

98 return {"defined": False} 

99 

100 # Copy the user defined properties since they will be modified. 

101 # Depending on the context, the Excel UI property may be called 'line' 

102 # or 'border'. Internally they are the same so we handle both. 

103 if options.get("line"): 

104 line = copy.deepcopy(options["line"]) 

105 else: 

106 line = copy.deepcopy(options["border"]) 

107 

108 dash_types = { 

109 "solid": "solid", 

110 "round_dot": "sysDot", 

111 "square_dot": "sysDash", 

112 "dash": "dash", 

113 "dash_dot": "dashDot", 

114 "long_dash": "lgDash", 

115 "long_dash_dot": "lgDashDot", 

116 "long_dash_dot_dot": "lgDashDotDot", 

117 "dot": "dot", 

118 "system_dash_dot": "sysDashDot", 

119 "system_dash_dot_dot": "sysDashDotDot", 

120 } 

121 

122 # Check the dash type. 

123 dash_type = line.get("dash_type") 

124 

125 if dash_type is not None: 

126 if dash_type in dash_types: 

127 line["dash_type"] = dash_types[dash_type] 

128 else: 

129 warn(f"Unknown dash type '{dash_type}'") 

130 return {} 

131 

132 if line.get("color"): 

133 line["color"] = Color._from_value(line["color"]) 

134 

135 line["defined"] = True 

136 

137 return line 

138 

139 @staticmethod 

140 def _get_fill_properties(fill): 

141 # Convert user fill properties to the structure required internally. 

142 

143 if not fill: 

144 return {"defined": False} 

145 

146 # Copy the user defined properties since they will be modified. 

147 fill = copy.deepcopy(fill) 

148 

149 if fill.get("color"): 

150 fill["color"] = Color._from_value(fill["color"]) 

151 

152 fill["defined"] = True 

153 

154 return fill 

155 

156 @staticmethod 

157 def _get_pattern_properties(pattern): 

158 # Convert user defined pattern to the structure required internally. 

159 

160 if not pattern: 

161 return {} 

162 

163 # Copy the user defined properties since they will be modified. 

164 pattern = copy.deepcopy(pattern) 

165 

166 if not pattern.get("pattern"): 

167 warn("Pattern must include 'pattern'") 

168 return {} 

169 

170 if not pattern.get("fg_color"): 

171 warn("Pattern must include 'fg_color'") 

172 return {} 

173 

174 types = { 

175 "percent_5": "pct5", 

176 "percent_10": "pct10", 

177 "percent_20": "pct20", 

178 "percent_25": "pct25", 

179 "percent_30": "pct30", 

180 "percent_40": "pct40", 

181 "percent_50": "pct50", 

182 "percent_60": "pct60", 

183 "percent_70": "pct70", 

184 "percent_75": "pct75", 

185 "percent_80": "pct80", 

186 "percent_90": "pct90", 

187 "light_downward_diagonal": "ltDnDiag", 

188 "light_upward_diagonal": "ltUpDiag", 

189 "dark_downward_diagonal": "dkDnDiag", 

190 "dark_upward_diagonal": "dkUpDiag", 

191 "wide_downward_diagonal": "wdDnDiag", 

192 "wide_upward_diagonal": "wdUpDiag", 

193 "light_vertical": "ltVert", 

194 "light_horizontal": "ltHorz", 

195 "narrow_vertical": "narVert", 

196 "narrow_horizontal": "narHorz", 

197 "dark_vertical": "dkVert", 

198 "dark_horizontal": "dkHorz", 

199 "dashed_downward_diagonal": "dashDnDiag", 

200 "dashed_upward_diagonal": "dashUpDiag", 

201 "dashed_horizontal": "dashHorz", 

202 "dashed_vertical": "dashVert", 

203 "small_confetti": "smConfetti", 

204 "large_confetti": "lgConfetti", 

205 "zigzag": "zigZag", 

206 "wave": "wave", 

207 "diagonal_brick": "diagBrick", 

208 "horizontal_brick": "horzBrick", 

209 "weave": "weave", 

210 "plaid": "plaid", 

211 "divot": "divot", 

212 "dotted_grid": "dotGrid", 

213 "dotted_diamond": "dotDmnd", 

214 "shingle": "shingle", 

215 "trellis": "trellis", 

216 "sphere": "sphere", 

217 "small_grid": "smGrid", 

218 "large_grid": "lgGrid", 

219 "small_check": "smCheck", 

220 "large_check": "lgCheck", 

221 "outlined_diamond": "openDmnd", 

222 "solid_diamond": "solidDmnd", 

223 } 

224 

225 # Check for valid types. 

226 if pattern["pattern"] not in types: 

227 warn(f"unknown pattern type '{pattern['pattern']}'") 

228 return {} 

229 

230 pattern["pattern"] = types[pattern["pattern"]] 

231 

232 if pattern.get("fg_color"): 

233 pattern["fg_color"] = Color._from_value(pattern["fg_color"]) 

234 

235 if pattern.get("bg_color"): 

236 pattern["bg_color"] = Color._from_value(pattern["bg_color"]) 

237 else: 

238 pattern["bg_color"] = Color("#FFFFFF") 

239 

240 return pattern 

241 

242 @staticmethod 

243 def _get_gradient_properties(gradient): 

244 # pylint: disable=too-many-return-statements 

245 # Convert user defined gradient to the structure required internally. 

246 

247 if not gradient: 

248 return {} 

249 

250 # Copy the user defined properties since they will be modified. 

251 gradient = copy.deepcopy(gradient) 

252 

253 types = { 

254 "linear": "linear", 

255 "radial": "circle", 

256 "rectangular": "rect", 

257 "path": "shape", 

258 } 

259 

260 # Check the colors array exists and is valid. 

261 if "colors" not in gradient or not isinstance(gradient["colors"], list): 

262 warn("Gradient must include colors list") 

263 return {} 

264 

265 # Check the colors array has the required number of entries. 

266 if not 2 <= len(gradient["colors"]) <= 10: 

267 warn("Gradient colors list must at least 2 values and not more than 10") 

268 return {} 

269 

270 if "positions" in gradient: 

271 # Check the positions array has the right number of entries. 

272 if len(gradient["positions"]) != len(gradient["colors"]): 

273 warn("Gradient positions not equal to number of colors") 

274 return {} 

275 

276 # Check the positions are in the correct range. 

277 for pos in gradient["positions"]: 

278 if not 0 <= pos <= 100: 

279 warn("Gradient position must be in the range 0 <= position <= 100") 

280 return {} 

281 else: 

282 # Use the default gradient positions. 

283 if len(gradient["colors"]) == 2: 

284 gradient["positions"] = [0, 100] 

285 

286 elif len(gradient["colors"]) == 3: 

287 gradient["positions"] = [0, 50, 100] 

288 

289 elif len(gradient["colors"]) == 4: 

290 gradient["positions"] = [0, 33, 66, 100] 

291 

292 else: 

293 warn("Must specify gradient positions") 

294 return {} 

295 

296 angle = gradient.get("angle") 

297 if angle: 

298 if not 0 <= angle < 360: 

299 warn("Gradient angle must be in the range 0 <= angle < 360") 

300 return {} 

301 else: 

302 gradient["angle"] = 90 

303 

304 # Check for valid types. 

305 gradient_type = gradient.get("type") 

306 

307 if gradient_type is not None: 

308 if gradient_type in types: 

309 gradient["type"] = types[gradient_type] 

310 else: 

311 warn(f"Unknown gradient type '{gradient_type}") 

312 return {} 

313 else: 

314 gradient["type"] = "linear" 

315 

316 gradient["colors"] = [Color._from_value(color) for color in gradient["colors"]] 

317 

318 return gradient 

319 

320 @staticmethod 

321 def _get_font_properties(options): 

322 # Convert user defined font values into private dict values. 

323 if options is None: 

324 options = {} 

325 

326 font = { 

327 "name": options.get("name"), 

328 "color": options.get("color"), 

329 "size": options.get("size", 11), 

330 "bold": options.get("bold"), 

331 "italic": options.get("italic"), 

332 "underline": options.get("underline"), 

333 "pitch_family": options.get("pitch_family"), 

334 "charset": options.get("charset"), 

335 "baseline": options.get("baseline", -1), 

336 "lang": options.get("lang", "en-US"), 

337 } 

338 

339 # Convert font size units. 

340 if font["size"]: 

341 font["size"] = int(font["size"] * 100) 

342 

343 if font.get("color"): 

344 font["color"] = Color._from_value(font["color"]) 

345 

346 return font 

347 

348 @staticmethod 

349 def _get_font_style_attributes(font): 

350 # _get_font_style_attributes. 

351 attributes = [] 

352 

353 if not font: 

354 return attributes 

355 

356 if font.get("size"): 

357 attributes.append(("sz", font["size"])) 

358 

359 if font.get("bold") is not None: 

360 attributes.append(("b", 0 + font["bold"])) 

361 

362 if font.get("italic") is not None: 

363 attributes.append(("i", 0 + font["italic"])) 

364 

365 if font.get("underline") is not None: 

366 attributes.append(("u", "sng")) 

367 

368 if font.get("baseline") != -1: 

369 attributes.append(("baseline", font["baseline"])) 

370 

371 return attributes 

372 

373 @staticmethod 

374 def _get_font_latin_attributes(font): 

375 # _get_font_latin_attributes. 

376 attributes = [] 

377 

378 if not font: 

379 return attributes 

380 

381 if font.get("name") is not None: 

382 attributes.append(("typeface", font["name"])) 

383 

384 if font.get("pitch_family") is not None: 

385 attributes.append(("pitchFamily", font["pitch_family"])) 

386 

387 if font.get("charset") is not None: 

388 attributes.append(("charset", font["charset"])) 

389 

390 return attributes 

391 

392 @staticmethod 

393 def _get_align_properties(align): 

394 # Convert user defined align to the structure required internally. 

395 if not align: 

396 return {"defined": False} 

397 

398 # Copy the user defined properties since they will be modified. 

399 align = copy.deepcopy(align) 

400 

401 if "vertical" in align: 

402 align_type = align["vertical"] 

403 

404 align_types = { 

405 "top": "top", 

406 "middle": "middle", 

407 "bottom": "bottom", 

408 } 

409 

410 if align_type in align_types: 

411 align["vertical"] = align_types[align_type] 

412 else: 

413 warn(f"Unknown alignment type '{align_type}'") 

414 return {"defined": False} 

415 

416 if "horizontal" in align: 

417 align_type = align["horizontal"] 

418 

419 align_types = { 

420 "left": "left", 

421 "center": "center", 

422 "right": "right", 

423 } 

424 

425 if align_type in align_types: 

426 align["horizontal"] = align_types[align_type] 

427 else: 

428 warn(f"Unknown alignment type '{align_type}'") 

429 return {"defined": False} 

430 

431 align["defined"] = True 

432 

433 return align