Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/xlsxwriter/utility.py: 20%

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

340 statements  

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

2# 

3# Worksheet - A class for writing Excel Worksheets. 

4# 

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

6# Copyright 2013-2024, John McNamara, jmcnamara@cpan.org 

7# 

8import datetime 

9import hashlib 

10import os 

11import re 

12from struct import unpack 

13from warnings import warn 

14from .exceptions import UndefinedImageSize 

15from .exceptions import UnsupportedImageFormat 

16 

17COL_NAMES = {} 

18 

19CHAR_WIDTHS = { 

20 " ": 3, 

21 "!": 5, 

22 '"': 6, 

23 "#": 7, 

24 "$": 7, 

25 "%": 11, 

26 "&": 10, 

27 "'": 3, 

28 "(": 5, 

29 ")": 5, 

30 "*": 7, 

31 "+": 7, 

32 ",": 4, 

33 "-": 5, 

34 ".": 4, 

35 "/": 6, 

36 "0": 7, 

37 "1": 7, 

38 "2": 7, 

39 "3": 7, 

40 "4": 7, 

41 "5": 7, 

42 "6": 7, 

43 "7": 7, 

44 "8": 7, 

45 "9": 7, 

46 ":": 4, 

47 ";": 4, 

48 "<": 7, 

49 "=": 7, 

50 ">": 7, 

51 "?": 7, 

52 "@": 13, 

53 "A": 9, 

54 "B": 8, 

55 "C": 8, 

56 "D": 9, 

57 "E": 7, 

58 "F": 7, 

59 "G": 9, 

60 "H": 9, 

61 "I": 4, 

62 "J": 5, 

63 "K": 8, 

64 "L": 6, 

65 "M": 12, 

66 "N": 10, 

67 "O": 10, 

68 "P": 8, 

69 "Q": 10, 

70 "R": 8, 

71 "S": 7, 

72 "T": 7, 

73 "U": 9, 

74 "V": 9, 

75 "W": 13, 

76 "X": 8, 

77 "Y": 7, 

78 "Z": 7, 

79 "[": 5, 

80 "\\": 6, 

81 "]": 5, 

82 "^": 7, 

83 "_": 7, 

84 "`": 4, 

85 "a": 7, 

86 "b": 8, 

87 "c": 6, 

88 "d": 8, 

89 "e": 8, 

90 "f": 5, 

91 "g": 7, 

92 "h": 8, 

93 "i": 4, 

94 "j": 4, 

95 "k": 7, 

96 "l": 4, 

97 "m": 12, 

98 "n": 8, 

99 "o": 8, 

100 "p": 8, 

101 "q": 8, 

102 "r": 5, 

103 "s": 6, 

104 "t": 5, 

105 "u": 8, 

106 "v": 7, 

107 "w": 11, 

108 "x": 7, 

109 "y": 7, 

110 "z": 6, 

111 "{": 5, 

112 "|": 7, 

113 "}": 5, 

114 "~": 7, 

115} 

116 

117# The following is a list of Emojis used to decide if worksheet names require 

118# quoting since there is (currently) no native support for matching them in 

119# Python regular expressions. It is probably unnecessary to exclude them since 

120# the default quoting is safe in Excel even when unnecessary (the reverse isn't 

121# true). The Emoji list was generated from: 

122# 

123# https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D&abb=on&esc=on&g=&i= 

124# 

125emojis = "\u00A9\u00AE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299\U0001F004\U0001F0CF\U0001F170\U0001F171\U0001F17E\U0001F17F\U0001F18E\U0001F191-\U0001F19A\U0001F1E6-\U0001F1FF\U0001F201\U0001F202\U0001F21A\U0001F22F\U0001F232-\U0001F23A\U0001F250\U0001F251\U0001F300-\U0001F321\U0001F324-\U0001F393\U0001F396\U0001F397\U0001F399-\U0001F39B\U0001F39E-\U0001F3F0\U0001F3F3-\U0001F3F5\U0001F3F7-\U0001F4FD\U0001F4FF-\U0001F53D\U0001F549-\U0001F54E\U0001F550-\U0001F567\U0001F56F\U0001F570\U0001F573-\U0001F57A\U0001F587\U0001F58A-\U0001F58D\U0001F590\U0001F595\U0001F596\U0001F5A4\U0001F5A5\U0001F5A8\U0001F5B1\U0001F5B2\U0001F5BC\U0001F5C2-\U0001F5C4\U0001F5D1-\U0001F5D3\U0001F5DC-\U0001F5DE\U0001F5E1\U0001F5E3\U0001F5E8\U0001F5EF\U0001F5F3\U0001F5FA-\U0001F64F\U0001F680-\U0001F6C5\U0001F6CB-\U0001F6D2\U0001F6D5-\U0001F6D7\U0001F6DC-\U0001F6E5\U0001F6E9\U0001F6EB\U0001F6EC\U0001F6F0\U0001F6F3-\U0001F6FC\U0001F7E0-\U0001F7EB\U0001F7F0\U0001F90C-\U0001F93A\U0001F93C-\U0001F945\U0001F947-\U0001F9FF\U0001FA70-\U0001FA7C\U0001FA80-\U0001FA88\U0001FA90-\U0001FABD\U0001FABF-\U0001FAC5\U0001FACE-\U0001FADB\U0001FAE0-\U0001FAE8\U0001FAF0-\U0001FAF8" # noqa 

126 

127# Compile performance critical regular expressions. 

128re_leading = re.compile(r"^\s") 

129re_trailing = re.compile(r"\s$") 

130re_range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)") 

131re_quote_rule1 = re.compile(rf"[^\w\.{emojis}]") 

132re_quote_rule2 = re.compile(rf"^[\d\.{emojis}]") 

133re_quote_rule3 = re.compile(r"^([A-Z]{1,3}\d+)$") 

134re_quote_rule4_row = re.compile(r"^R(\d+)") 

135re_quote_rule4_column = re.compile(r"^R?C(\d+)") 

136 

137 

138def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False): 

139 """ 

140 Convert a zero indexed row and column cell reference to a A1 style string. 

141 

142 Args: 

143 row: The cell row. Int. 

144 col: The cell column. Int. 

145 row_abs: Optional flag to make the row absolute. Bool. 

146 col_abs: Optional flag to make the column absolute. Bool. 

147 

148 Returns: 

149 A1 style string. 

150 

151 """ 

152 if row < 0: 

153 warn("Row number %d must be >= 0" % row) 

154 return None 

155 

156 if col < 0: 

157 warn("Col number %d must be >= 0" % col) 

158 return None 

159 

160 row += 1 # Change to 1-index. 

161 row_abs = "$" if row_abs else "" 

162 

163 col_str = xl_col_to_name(col, col_abs) 

164 

165 return col_str + row_abs + str(row) 

166 

167 

168def xl_rowcol_to_cell_fast(row, col): 

169 """ 

170 Optimized version of the xl_rowcol_to_cell function. Only used internally. 

171 

172 Args: 

173 row: The cell row. Int. 

174 col: The cell column. Int. 

175 

176 Returns: 

177 A1 style string. 

178 

179 """ 

180 if col in COL_NAMES: 

181 col_str = COL_NAMES[col] 

182 else: 

183 col_str = xl_col_to_name(col) 

184 COL_NAMES[col] = col_str 

185 

186 return col_str + str(row + 1) 

187 

188 

189def xl_col_to_name(col, col_abs=False): 

190 """ 

191 Convert a zero indexed column cell reference to a string. 

192 

193 Args: 

194 col: The cell column. Int. 

195 col_abs: Optional flag to make the column absolute. Bool. 

196 

197 Returns: 

198 Column style string. 

199 

200 """ 

201 col_num = col 

202 if col_num < 0: 

203 warn("Col number %d must be >= 0" % col_num) 

204 return None 

205 

206 col_num += 1 # Change to 1-index. 

207 col_str = "" 

208 col_abs = "$" if col_abs else "" 

209 

210 while col_num: 

211 # Set remainder from 1 .. 26 

212 remainder = col_num % 26 

213 

214 if remainder == 0: 

215 remainder = 26 

216 

217 # Convert the remainder to a character. 

218 col_letter = chr(ord("A") + remainder - 1) 

219 

220 # Accumulate the column letters, right to left. 

221 col_str = col_letter + col_str 

222 

223 # Get the next order of magnitude. 

224 col_num = int((col_num - 1) / 26) 

225 

226 return col_abs + col_str 

227 

228 

229def xl_cell_to_rowcol(cell_str): 

230 """ 

231 Convert a cell reference in A1 notation to a zero indexed row and column. 

232 

233 Args: 

234 cell_str: A1 style string. 

235 

236 Returns: 

237 row, col: Zero indexed cell row and column indices. 

238 

239 """ 

240 if not cell_str: 

241 return 0, 0 

242 

243 match = re_range_parts.match(cell_str) 

244 col_str = match.group(2) 

245 row_str = match.group(4) 

246 

247 # Convert base26 column string to number. 

248 expn = 0 

249 col = 0 

250 for char in reversed(col_str): 

251 col += (ord(char) - ord("A") + 1) * (26**expn) 

252 expn += 1 

253 

254 # Convert 1-index to zero-index 

255 row = int(row_str) - 1 

256 col -= 1 

257 

258 return row, col 

259 

260 

261def xl_cell_to_rowcol_abs(cell_str): 

262 """ 

263 Convert an absolute cell reference in A1 notation to a zero indexed 

264 row and column, with True/False values for absolute rows or columns. 

265 

266 Args: 

267 cell_str: A1 style string. 

268 

269 Returns: 

270 row, col, row_abs, col_abs: Zero indexed cell row and column indices. 

271 

272 """ 

273 if not cell_str: 

274 return 0, 0, False, False 

275 

276 match = re_range_parts.match(cell_str) 

277 

278 col_abs = match.group(1) 

279 col_str = match.group(2) 

280 row_abs = match.group(3) 

281 row_str = match.group(4) 

282 

283 if col_abs: 

284 col_abs = True 

285 else: 

286 col_abs = False 

287 

288 if row_abs: 

289 row_abs = True 

290 else: 

291 row_abs = False 

292 

293 # Convert base26 column string to number. 

294 expn = 0 

295 col = 0 

296 for char in reversed(col_str): 

297 col += (ord(char) - ord("A") + 1) * (26**expn) 

298 expn += 1 

299 

300 # Convert 1-index to zero-index 

301 row = int(row_str) - 1 

302 col -= 1 

303 

304 return row, col, row_abs, col_abs 

305 

306 

307def xl_range(first_row, first_col, last_row, last_col): 

308 """ 

309 Convert zero indexed row and col cell references to a A1:B1 range string. 

310 

311 Args: 

312 first_row: The first cell row. Int. 

313 first_col: The first cell column. Int. 

314 last_row: The last cell row. Int. 

315 last_col: The last cell column. Int. 

316 

317 Returns: 

318 A1:B1 style range string. 

319 

320 """ 

321 range1 = xl_rowcol_to_cell(first_row, first_col) 

322 range2 = xl_rowcol_to_cell(last_row, last_col) 

323 

324 if range1 is None or range2 is None: 

325 warn("Row and column numbers must be >= 0") 

326 return None 

327 

328 if range1 == range2: 

329 return range1 

330 else: 

331 return range1 + ":" + range2 

332 

333 

334def xl_range_abs(first_row, first_col, last_row, last_col): 

335 """ 

336 Convert zero indexed row and col cell references to a $A$1:$B$1 absolute 

337 range string. 

338 

339 Args: 

340 first_row: The first cell row. Int. 

341 first_col: The first cell column. Int. 

342 last_row: The last cell row. Int. 

343 last_col: The last cell column. Int. 

344 

345 Returns: 

346 $A$1:$B$1 style range string. 

347 

348 """ 

349 range1 = xl_rowcol_to_cell(first_row, first_col, True, True) 

350 range2 = xl_rowcol_to_cell(last_row, last_col, True, True) 

351 

352 if range1 is None or range2 is None: 

353 warn("Row and column numbers must be >= 0") 

354 return None 

355 

356 if range1 == range2: 

357 return range1 

358 else: 

359 return range1 + ":" + range2 

360 

361 

362def xl_range_formula(sheetname, first_row, first_col, last_row, last_col): 

363 """ 

364 Convert worksheet name and zero indexed row and col cell references to 

365 a Sheet1!A1:B1 range formula string. 

366 

367 Args: 

368 sheetname: The worksheet name. String. 

369 first_row: The first cell row. Int. 

370 first_col: The first cell column. Int. 

371 last_row: The last cell row. Int. 

372 last_col: The last cell column. Int. 

373 

374 Returns: 

375 A1:B1 style range string. 

376 

377 """ 

378 cell_range = xl_range_abs(first_row, first_col, last_row, last_col) 

379 sheetname = quote_sheetname(sheetname) 

380 

381 return sheetname + "!" + cell_range 

382 

383 

384def quote_sheetname(sheetname): 

385 """ 

386 Sheetnames used in references should be quoted if they contain any spaces, 

387 special characters or if they look like a A1 or RC cell reference. The rules 

388 are shown inline below. 

389 

390 Args: 

391 sheetname: The worksheet name. String. 

392 

393 Returns: 

394 A quoted worksheet string. 

395 

396 """ 

397 uppercase_sheetname = sheetname.upper() 

398 requires_quoting = False 

399 col_max = 163_84 

400 row_max = 1048576 

401 

402 # Don't quote sheetname if it is already quoted by the user. 

403 if not sheetname.startswith("'"): 

404 

405 # -------------------------------------------------------------------- 

406 # Rule 1. Sheet names that contain anything other than \w and "." 

407 # characters must be quoted. 

408 # -------------------------------------------------------------------- 

409 if re_quote_rule1.search(sheetname): 

410 requires_quoting = True 

411 

412 # -------------------------------------------------------------------- 

413 # Rule 2. Sheet names that start with a digit or "." must be quoted. 

414 # -------------------------------------------------------------------- 

415 elif re_quote_rule2.search(sheetname): 

416 requires_quoting = True 

417 

418 # -------------------------------------------------------------------- 

419 # Rule 3. Sheet names must not be a valid A1 style cell reference. 

420 # Valid means that the row and column range values must also be within 

421 # Excel row and column limits. 

422 # -------------------------------------------------------------------- 

423 elif re_quote_rule3.match(uppercase_sheetname): 

424 match = re_quote_rule3.match(uppercase_sheetname) 

425 cell = match.group(1) 

426 (row, col) = xl_cell_to_rowcol(cell) 

427 

428 if row >= 0 and row < row_max and col >= 0 and col < col_max: 

429 requires_quoting = True 

430 

431 # -------------------------------------------------------------------- 

432 # Rule 4. Sheet names must not *start* with a valid RC style cell 

433 # reference. Other characters after the valid RC reference are ignored 

434 # by Excel. Valid means that the row and column range values must also 

435 # be within Excel row and column limits. 

436 # 

437 # Note: references without trailing characters like R12345 or C12345 

438 # are caught by Rule 3. Negative references like R-12345 are caught by 

439 # Rule 1 due to dash. 

440 # -------------------------------------------------------------------- 

441 

442 # Rule 4a. Check for sheet names that start with R1 style references. 

443 elif re_quote_rule4_row.match(uppercase_sheetname): 

444 match = re_quote_rule4_row.match(uppercase_sheetname) 

445 row = int(match.group(1)) 

446 

447 if row > 0 and row <= row_max: 

448 requires_quoting = True 

449 

450 # Rule 4b. Check for sheet names that start with C1 or RC1 style 

451 elif re_quote_rule4_column.match(uppercase_sheetname): 

452 match = re_quote_rule4_column.match(uppercase_sheetname) 

453 col = int(match.group(1)) 

454 

455 if col > 0 and col <= col_max: 

456 requires_quoting = True 

457 

458 # Rule 4c. Check for some single R/C references. 

459 elif uppercase_sheetname in ("R", "C", "RC"): 

460 requires_quoting = True 

461 

462 if requires_quoting: 

463 # Double quote any single quotes. 

464 sheetname = sheetname.replace("'", "''") 

465 

466 # Single quote the sheet name. 

467 sheetname = "'%s'" % sheetname 

468 

469 return sheetname 

470 

471 

472def xl_pixel_width(string): 

473 """ 

474 Get the pixel width of a string based on individual character widths taken 

475 from Excel. UTF8 characters, and other unhandled characters, are given a 

476 default width of 8. 

477 

478 Args: 

479 string: The string to calculate the width for. String. 

480 

481 Returns: 

482 The string width in pixels. Note, Excel adds an additional 7 pixels of 

483 padding in the cell. 

484 

485 """ 

486 length = 0 

487 for char in string: 

488 length += CHAR_WIDTHS.get(char, 8) 

489 

490 return length 

491 

492 

493def xl_color(color): 

494 # Used in conjunction with the XlsxWriter *color() methods to convert 

495 # a color name into an RGB formatted string. These colors are for 

496 # backward compatibility with older versions of Excel. 

497 named_colors = { 

498 "black": "#000000", 

499 "blue": "#0000FF", 

500 "brown": "#800000", 

501 "cyan": "#00FFFF", 

502 "gray": "#808080", 

503 "green": "#008000", 

504 "lime": "#00FF00", 

505 "magenta": "#FF00FF", 

506 "navy": "#000080", 

507 "orange": "#FF6600", 

508 "pink": "#FF00FF", 

509 "purple": "#800080", 

510 "red": "#FF0000", 

511 "silver": "#C0C0C0", 

512 "white": "#FFFFFF", 

513 "yellow": "#FFFF00", 

514 } 

515 

516 if color in named_colors: 

517 color = named_colors[color] 

518 

519 if not re.match("#[0-9a-fA-F]{6}", color): 

520 warn("Color '%s' isn't a valid Excel color" % color) 

521 

522 # Convert the RGB color to the Excel ARGB format. 

523 return "FF" + color.lstrip("#").upper() 

524 

525 

526def get_rgb_color(color): 

527 # Convert the user specified color to an RGB color. 

528 rgb_color = xl_color(color) 

529 

530 # Remove leading FF from RGB color for charts. 

531 rgb_color = re.sub(r"^FF", "", rgb_color) 

532 

533 return rgb_color 

534 

535 

536def get_sparkline_style(style_id): 

537 styles = [ 

538 { 

539 "series": {"theme": "4", "tint": "-0.499984740745262"}, 

540 "negative": {"theme": "5"}, 

541 "markers": {"theme": "4", "tint": "-0.499984740745262"}, 

542 "first": {"theme": "4", "tint": "0.39997558519241921"}, 

543 "last": {"theme": "4", "tint": "0.39997558519241921"}, 

544 "high": {"theme": "4"}, 

545 "low": {"theme": "4"}, 

546 }, # 0 

547 { 

548 "series": {"theme": "4", "tint": "-0.499984740745262"}, 

549 "negative": {"theme": "5"}, 

550 "markers": {"theme": "4", "tint": "-0.499984740745262"}, 

551 "first": {"theme": "4", "tint": "0.39997558519241921"}, 

552 "last": {"theme": "4", "tint": "0.39997558519241921"}, 

553 "high": {"theme": "4"}, 

554 "low": {"theme": "4"}, 

555 }, # 1 

556 { 

557 "series": {"theme": "5", "tint": "-0.499984740745262"}, 

558 "negative": {"theme": "6"}, 

559 "markers": {"theme": "5", "tint": "-0.499984740745262"}, 

560 "first": {"theme": "5", "tint": "0.39997558519241921"}, 

561 "last": {"theme": "5", "tint": "0.39997558519241921"}, 

562 "high": {"theme": "5"}, 

563 "low": {"theme": "5"}, 

564 }, # 2 

565 { 

566 "series": {"theme": "6", "tint": "-0.499984740745262"}, 

567 "negative": {"theme": "7"}, 

568 "markers": {"theme": "6", "tint": "-0.499984740745262"}, 

569 "first": {"theme": "6", "tint": "0.39997558519241921"}, 

570 "last": {"theme": "6", "tint": "0.39997558519241921"}, 

571 "high": {"theme": "6"}, 

572 "low": {"theme": "6"}, 

573 }, # 3 

574 { 

575 "series": {"theme": "7", "tint": "-0.499984740745262"}, 

576 "negative": {"theme": "8"}, 

577 "markers": {"theme": "7", "tint": "-0.499984740745262"}, 

578 "first": {"theme": "7", "tint": "0.39997558519241921"}, 

579 "last": {"theme": "7", "tint": "0.39997558519241921"}, 

580 "high": {"theme": "7"}, 

581 "low": {"theme": "7"}, 

582 }, # 4 

583 { 

584 "series": {"theme": "8", "tint": "-0.499984740745262"}, 

585 "negative": {"theme": "9"}, 

586 "markers": {"theme": "8", "tint": "-0.499984740745262"}, 

587 "first": {"theme": "8", "tint": "0.39997558519241921"}, 

588 "last": {"theme": "8", "tint": "0.39997558519241921"}, 

589 "high": {"theme": "8"}, 

590 "low": {"theme": "8"}, 

591 }, # 5 

592 { 

593 "series": {"theme": "9", "tint": "-0.499984740745262"}, 

594 "negative": {"theme": "4"}, 

595 "markers": {"theme": "9", "tint": "-0.499984740745262"}, 

596 "first": {"theme": "9", "tint": "0.39997558519241921"}, 

597 "last": {"theme": "9", "tint": "0.39997558519241921"}, 

598 "high": {"theme": "9"}, 

599 "low": {"theme": "9"}, 

600 }, # 6 

601 { 

602 "series": {"theme": "4", "tint": "-0.249977111117893"}, 

603 "negative": {"theme": "5"}, 

604 "markers": {"theme": "5", "tint": "-0.249977111117893"}, 

605 "first": {"theme": "5", "tint": "-0.249977111117893"}, 

606 "last": {"theme": "5", "tint": "-0.249977111117893"}, 

607 "high": {"theme": "5", "tint": "-0.249977111117893"}, 

608 "low": {"theme": "5", "tint": "-0.249977111117893"}, 

609 }, # 7 

610 { 

611 "series": {"theme": "5", "tint": "-0.249977111117893"}, 

612 "negative": {"theme": "6"}, 

613 "markers": {"theme": "6", "tint": "-0.249977111117893"}, 

614 "first": {"theme": "6", "tint": "-0.249977111117893"}, 

615 "last": {"theme": "6", "tint": "-0.249977111117893"}, 

616 "high": {"theme": "6", "tint": "-0.249977111117893"}, 

617 "low": {"theme": "6", "tint": "-0.249977111117893"}, 

618 }, # 8 

619 { 

620 "series": {"theme": "6", "tint": "-0.249977111117893"}, 

621 "negative": {"theme": "7"}, 

622 "markers": {"theme": "7", "tint": "-0.249977111117893"}, 

623 "first": {"theme": "7", "tint": "-0.249977111117893"}, 

624 "last": {"theme": "7", "tint": "-0.249977111117893"}, 

625 "high": {"theme": "7", "tint": "-0.249977111117893"}, 

626 "low": {"theme": "7", "tint": "-0.249977111117893"}, 

627 }, # 9 

628 { 

629 "series": {"theme": "7", "tint": "-0.249977111117893"}, 

630 "negative": {"theme": "8"}, 

631 "markers": {"theme": "8", "tint": "-0.249977111117893"}, 

632 "first": {"theme": "8", "tint": "-0.249977111117893"}, 

633 "last": {"theme": "8", "tint": "-0.249977111117893"}, 

634 "high": {"theme": "8", "tint": "-0.249977111117893"}, 

635 "low": {"theme": "8", "tint": "-0.249977111117893"}, 

636 }, # 10 

637 { 

638 "series": {"theme": "8", "tint": "-0.249977111117893"}, 

639 "negative": {"theme": "9"}, 

640 "markers": {"theme": "9", "tint": "-0.249977111117893"}, 

641 "first": {"theme": "9", "tint": "-0.249977111117893"}, 

642 "last": {"theme": "9", "tint": "-0.249977111117893"}, 

643 "high": {"theme": "9", "tint": "-0.249977111117893"}, 

644 "low": {"theme": "9", "tint": "-0.249977111117893"}, 

645 }, # 11 

646 { 

647 "series": {"theme": "9", "tint": "-0.249977111117893"}, 

648 "negative": {"theme": "4"}, 

649 "markers": {"theme": "4", "tint": "-0.249977111117893"}, 

650 "first": {"theme": "4", "tint": "-0.249977111117893"}, 

651 "last": {"theme": "4", "tint": "-0.249977111117893"}, 

652 "high": {"theme": "4", "tint": "-0.249977111117893"}, 

653 "low": {"theme": "4", "tint": "-0.249977111117893"}, 

654 }, # 12 

655 { 

656 "series": {"theme": "4"}, 

657 "negative": {"theme": "5"}, 

658 "markers": {"theme": "4", "tint": "-0.249977111117893"}, 

659 "first": {"theme": "4", "tint": "-0.249977111117893"}, 

660 "last": {"theme": "4", "tint": "-0.249977111117893"}, 

661 "high": {"theme": "4", "tint": "-0.249977111117893"}, 

662 "low": {"theme": "4", "tint": "-0.249977111117893"}, 

663 }, # 13 

664 { 

665 "series": {"theme": "5"}, 

666 "negative": {"theme": "6"}, 

667 "markers": {"theme": "5", "tint": "-0.249977111117893"}, 

668 "first": {"theme": "5", "tint": "-0.249977111117893"}, 

669 "last": {"theme": "5", "tint": "-0.249977111117893"}, 

670 "high": {"theme": "5", "tint": "-0.249977111117893"}, 

671 "low": {"theme": "5", "tint": "-0.249977111117893"}, 

672 }, # 14 

673 { 

674 "series": {"theme": "6"}, 

675 "negative": {"theme": "7"}, 

676 "markers": {"theme": "6", "tint": "-0.249977111117893"}, 

677 "first": {"theme": "6", "tint": "-0.249977111117893"}, 

678 "last": {"theme": "6", "tint": "-0.249977111117893"}, 

679 "high": {"theme": "6", "tint": "-0.249977111117893"}, 

680 "low": {"theme": "6", "tint": "-0.249977111117893"}, 

681 }, # 15 

682 { 

683 "series": {"theme": "7"}, 

684 "negative": {"theme": "8"}, 

685 "markers": {"theme": "7", "tint": "-0.249977111117893"}, 

686 "first": {"theme": "7", "tint": "-0.249977111117893"}, 

687 "last": {"theme": "7", "tint": "-0.249977111117893"}, 

688 "high": {"theme": "7", "tint": "-0.249977111117893"}, 

689 "low": {"theme": "7", "tint": "-0.249977111117893"}, 

690 }, # 16 

691 { 

692 "series": {"theme": "8"}, 

693 "negative": {"theme": "9"}, 

694 "markers": {"theme": "8", "tint": "-0.249977111117893"}, 

695 "first": {"theme": "8", "tint": "-0.249977111117893"}, 

696 "last": {"theme": "8", "tint": "-0.249977111117893"}, 

697 "high": {"theme": "8", "tint": "-0.249977111117893"}, 

698 "low": {"theme": "8", "tint": "-0.249977111117893"}, 

699 }, # 17 

700 { 

701 "series": {"theme": "9"}, 

702 "negative": {"theme": "4"}, 

703 "markers": {"theme": "9", "tint": "-0.249977111117893"}, 

704 "first": {"theme": "9", "tint": "-0.249977111117893"}, 

705 "last": {"theme": "9", "tint": "-0.249977111117893"}, 

706 "high": {"theme": "9", "tint": "-0.249977111117893"}, 

707 "low": {"theme": "9", "tint": "-0.249977111117893"}, 

708 }, # 18 

709 { 

710 "series": {"theme": "4", "tint": "0.39997558519241921"}, 

711 "negative": {"theme": "0", "tint": "-0.499984740745262"}, 

712 "markers": {"theme": "4", "tint": "0.79998168889431442"}, 

713 "first": {"theme": "4", "tint": "-0.249977111117893"}, 

714 "last": {"theme": "4", "tint": "-0.249977111117893"}, 

715 "high": {"theme": "4", "tint": "-0.499984740745262"}, 

716 "low": {"theme": "4", "tint": "-0.499984740745262"}, 

717 }, # 19 

718 { 

719 "series": {"theme": "5", "tint": "0.39997558519241921"}, 

720 "negative": {"theme": "0", "tint": "-0.499984740745262"}, 

721 "markers": {"theme": "5", "tint": "0.79998168889431442"}, 

722 "first": {"theme": "5", "tint": "-0.249977111117893"}, 

723 "last": {"theme": "5", "tint": "-0.249977111117893"}, 

724 "high": {"theme": "5", "tint": "-0.499984740745262"}, 

725 "low": {"theme": "5", "tint": "-0.499984740745262"}, 

726 }, # 20 

727 { 

728 "series": {"theme": "6", "tint": "0.39997558519241921"}, 

729 "negative": {"theme": "0", "tint": "-0.499984740745262"}, 

730 "markers": {"theme": "6", "tint": "0.79998168889431442"}, 

731 "first": {"theme": "6", "tint": "-0.249977111117893"}, 

732 "last": {"theme": "6", "tint": "-0.249977111117893"}, 

733 "high": {"theme": "6", "tint": "-0.499984740745262"}, 

734 "low": {"theme": "6", "tint": "-0.499984740745262"}, 

735 }, # 21 

736 { 

737 "series": {"theme": "7", "tint": "0.39997558519241921"}, 

738 "negative": {"theme": "0", "tint": "-0.499984740745262"}, 

739 "markers": {"theme": "7", "tint": "0.79998168889431442"}, 

740 "first": {"theme": "7", "tint": "-0.249977111117893"}, 

741 "last": {"theme": "7", "tint": "-0.249977111117893"}, 

742 "high": {"theme": "7", "tint": "-0.499984740745262"}, 

743 "low": {"theme": "7", "tint": "-0.499984740745262"}, 

744 }, # 22 

745 { 

746 "series": {"theme": "8", "tint": "0.39997558519241921"}, 

747 "negative": {"theme": "0", "tint": "-0.499984740745262"}, 

748 "markers": {"theme": "8", "tint": "0.79998168889431442"}, 

749 "first": {"theme": "8", "tint": "-0.249977111117893"}, 

750 "last": {"theme": "8", "tint": "-0.249977111117893"}, 

751 "high": {"theme": "8", "tint": "-0.499984740745262"}, 

752 "low": {"theme": "8", "tint": "-0.499984740745262"}, 

753 }, # 23 

754 { 

755 "series": {"theme": "9", "tint": "0.39997558519241921"}, 

756 "negative": {"theme": "0", "tint": "-0.499984740745262"}, 

757 "markers": {"theme": "9", "tint": "0.79998168889431442"}, 

758 "first": {"theme": "9", "tint": "-0.249977111117893"}, 

759 "last": {"theme": "9", "tint": "-0.249977111117893"}, 

760 "high": {"theme": "9", "tint": "-0.499984740745262"}, 

761 "low": {"theme": "9", "tint": "-0.499984740745262"}, 

762 }, # 24 

763 { 

764 "series": {"theme": "1", "tint": "0.499984740745262"}, 

765 "negative": {"theme": "1", "tint": "0.249977111117893"}, 

766 "markers": {"theme": "1", "tint": "0.249977111117893"}, 

767 "first": {"theme": "1", "tint": "0.249977111117893"}, 

768 "last": {"theme": "1", "tint": "0.249977111117893"}, 

769 "high": {"theme": "1", "tint": "0.249977111117893"}, 

770 "low": {"theme": "1", "tint": "0.249977111117893"}, 

771 }, # 25 

772 { 

773 "series": {"theme": "1", "tint": "0.34998626667073579"}, 

774 "negative": {"theme": "0", "tint": "-0.249977111117893"}, 

775 "markers": {"theme": "0", "tint": "-0.249977111117893"}, 

776 "first": {"theme": "0", "tint": "-0.249977111117893"}, 

777 "last": {"theme": "0", "tint": "-0.249977111117893"}, 

778 "high": {"theme": "0", "tint": "-0.249977111117893"}, 

779 "low": {"theme": "0", "tint": "-0.249977111117893"}, 

780 }, # 26 

781 { 

782 "series": {"rgb": "FF323232"}, 

783 "negative": {"rgb": "FFD00000"}, 

784 "markers": {"rgb": "FFD00000"}, 

785 "first": {"rgb": "FFD00000"}, 

786 "last": {"rgb": "FFD00000"}, 

787 "high": {"rgb": "FFD00000"}, 

788 "low": {"rgb": "FFD00000"}, 

789 }, # 27 

790 { 

791 "series": {"rgb": "FF000000"}, 

792 "negative": {"rgb": "FF0070C0"}, 

793 "markers": {"rgb": "FF0070C0"}, 

794 "first": {"rgb": "FF0070C0"}, 

795 "last": {"rgb": "FF0070C0"}, 

796 "high": {"rgb": "FF0070C0"}, 

797 "low": {"rgb": "FF0070C0"}, 

798 }, # 28 

799 { 

800 "series": {"rgb": "FF376092"}, 

801 "negative": {"rgb": "FFD00000"}, 

802 "markers": {"rgb": "FFD00000"}, 

803 "first": {"rgb": "FFD00000"}, 

804 "last": {"rgb": "FFD00000"}, 

805 "high": {"rgb": "FFD00000"}, 

806 "low": {"rgb": "FFD00000"}, 

807 }, # 29 

808 { 

809 "series": {"rgb": "FF0070C0"}, 

810 "negative": {"rgb": "FF000000"}, 

811 "markers": {"rgb": "FF000000"}, 

812 "first": {"rgb": "FF000000"}, 

813 "last": {"rgb": "FF000000"}, 

814 "high": {"rgb": "FF000000"}, 

815 "low": {"rgb": "FF000000"}, 

816 }, # 30 

817 { 

818 "series": {"rgb": "FF5F5F5F"}, 

819 "negative": {"rgb": "FFFFB620"}, 

820 "markers": {"rgb": "FFD70077"}, 

821 "first": {"rgb": "FF5687C2"}, 

822 "last": {"rgb": "FF359CEB"}, 

823 "high": {"rgb": "FF56BE79"}, 

824 "low": {"rgb": "FFFF5055"}, 

825 }, # 31 

826 { 

827 "series": {"rgb": "FF5687C2"}, 

828 "negative": {"rgb": "FFFFB620"}, 

829 "markers": {"rgb": "FFD70077"}, 

830 "first": {"rgb": "FF777777"}, 

831 "last": {"rgb": "FF359CEB"}, 

832 "high": {"rgb": "FF56BE79"}, 

833 "low": {"rgb": "FFFF5055"}, 

834 }, # 32 

835 { 

836 "series": {"rgb": "FFC6EFCE"}, 

837 "negative": {"rgb": "FFFFC7CE"}, 

838 "markers": {"rgb": "FF8CADD6"}, 

839 "first": {"rgb": "FFFFDC47"}, 

840 "last": {"rgb": "FFFFEB9C"}, 

841 "high": {"rgb": "FF60D276"}, 

842 "low": {"rgb": "FFFF5367"}, 

843 }, # 33 

844 { 

845 "series": {"rgb": "FF00B050"}, 

846 "negative": {"rgb": "FFFF0000"}, 

847 "markers": {"rgb": "FF0070C0"}, 

848 "first": {"rgb": "FFFFC000"}, 

849 "last": {"rgb": "FFFFC000"}, 

850 "high": {"rgb": "FF00B050"}, 

851 "low": {"rgb": "FFFF0000"}, 

852 }, # 34 

853 { 

854 "series": {"theme": "3"}, 

855 "negative": {"theme": "9"}, 

856 "markers": {"theme": "8"}, 

857 "first": {"theme": "4"}, 

858 "last": {"theme": "5"}, 

859 "high": {"theme": "6"}, 

860 "low": {"theme": "7"}, 

861 }, # 35 

862 { 

863 "series": {"theme": "1"}, 

864 "negative": {"theme": "9"}, 

865 "markers": {"theme": "8"}, 

866 "first": {"theme": "4"}, 

867 "last": {"theme": "5"}, 

868 "high": {"theme": "6"}, 

869 "low": {"theme": "7"}, 

870 }, # 36 

871 ] 

872 

873 return styles[style_id] 

874 

875 

876def supported_datetime(dt): 

877 # Determine is an argument is a supported datetime object. 

878 return isinstance( 

879 dt, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) 

880 ) 

881 

882 

883def remove_datetime_timezone(dt_obj, remove_timezone): 

884 # Excel doesn't support timezones in datetimes/times so we remove the 

885 # tzinfo from the object if the user has specified that option in the 

886 # constructor. 

887 if remove_timezone: 

888 dt_obj = dt_obj.replace(tzinfo=None) 

889 else: 

890 if dt_obj.tzinfo: 

891 raise TypeError( 

892 "Excel doesn't support timezones in datetimes. " 

893 "Set the tzinfo in the datetime/time object to None or " 

894 "use the 'remove_timezone' Workbook() option" 

895 ) 

896 

897 return dt_obj 

898 

899 

900def datetime_to_excel_datetime(dt_obj, date_1904, remove_timezone): 

901 # Convert a datetime object to an Excel serial date and time. The integer 

902 # part of the number stores the number of days since the epoch and the 

903 # fractional part stores the percentage of the day. 

904 date_type = dt_obj 

905 is_timedelta = False 

906 

907 if date_1904: 

908 # Excel for Mac date epoch. 

909 epoch = datetime.datetime(1904, 1, 1) 

910 else: 

911 # Default Excel epoch. 

912 epoch = datetime.datetime(1899, 12, 31) 

913 

914 # We handle datetime .datetime, .date and .time objects but convert 

915 # them to datetime.datetime objects and process them in the same way. 

916 if isinstance(dt_obj, datetime.datetime): 

917 dt_obj = remove_datetime_timezone(dt_obj, remove_timezone) 

918 delta = dt_obj - epoch 

919 elif isinstance(dt_obj, datetime.date): 

920 dt_obj = datetime.datetime.fromordinal(dt_obj.toordinal()) 

921 delta = dt_obj - epoch 

922 elif isinstance(dt_obj, datetime.time): 

923 dt_obj = datetime.datetime.combine(epoch, dt_obj) 

924 dt_obj = remove_datetime_timezone(dt_obj, remove_timezone) 

925 delta = dt_obj - epoch 

926 elif isinstance(dt_obj, datetime.timedelta): 

927 is_timedelta = True 

928 delta = dt_obj 

929 else: 

930 raise TypeError("Unknown or unsupported datetime type") 

931 

932 # Convert a Python datetime.datetime value to an Excel date number. 

933 excel_time = delta.days + ( 

934 float(delta.seconds) + float(delta.microseconds) / 1e6 

935 ) / (60 * 60 * 24) 

936 

937 # The following is a workaround for the fact that in Excel a time only 

938 # value is represented as 1899-12-31+time whereas in datetime.datetime() 

939 # it is 1900-1-1+time so we need to subtract the 1 day difference. 

940 if isinstance(date_type, datetime.datetime) and dt_obj.isocalendar() == ( 

941 1900, 

942 1, 

943 1, 

944 ): 

945 excel_time -= 1 

946 

947 # Account for Excel erroneously treating 1900 as a leap year. 

948 if not date_1904 and not is_timedelta and excel_time > 59: 

949 excel_time += 1 

950 

951 return excel_time 

952 

953 

954def preserve_whitespace(string): 

955 # Check if a string has leading or trailing whitespace that requires a 

956 # "preserve" attribute. 

957 if re_leading.search(string) or re_trailing.search(string): 

958 return True 

959 else: 

960 return False 

961 

962 

963def get_image_properties(filename, image_data): 

964 # Extract dimension information from the image file. 

965 height = 0 

966 width = 0 

967 x_dpi = 96 

968 y_dpi = 96 

969 

970 if not image_data: 

971 # Open the image file and read in the data. 

972 fh = open(filename, "rb") 

973 data = fh.read() 

974 else: 

975 # Read the image data from the user supplied byte stream. 

976 data = image_data.getvalue() 

977 

978 digest = hashlib.sha256(data).hexdigest() 

979 

980 # Get the image filename without the path. 

981 image_name = os.path.basename(filename) 

982 

983 # Look for some common image file markers. 

984 marker1 = unpack("3s", data[1:4])[0] 

985 marker2 = unpack(">H", data[:2])[0] 

986 marker3 = unpack("2s", data[:2])[0] 

987 marker4 = unpack("<L", data[:4])[0] 

988 marker5 = (unpack("4s", data[40:44]))[0] 

989 marker6 = unpack("4s", data[:4])[0] 

990 

991 png_marker = b"PNG" 

992 bmp_marker = b"BM" 

993 emf_marker = b" EMF" 

994 gif_marker = b"GIF8" 

995 

996 if marker1 == png_marker: 

997 (image_type, width, height, x_dpi, y_dpi) = _process_png(data) 

998 

999 elif marker2 == 0xFFD8: 

1000 (image_type, width, height, x_dpi, y_dpi) = _process_jpg(data) 

1001 

1002 elif marker3 == bmp_marker: 

1003 (image_type, width, height) = _process_bmp(data) 

1004 

1005 elif marker4 == 0x9AC6CDD7: 

1006 (image_type, width, height, x_dpi, y_dpi) = _process_wmf(data) 

1007 

1008 elif marker4 == 1 and marker5 == emf_marker: 

1009 (image_type, width, height, x_dpi, y_dpi) = _process_emf(data) 

1010 

1011 elif marker6 == gif_marker: 

1012 (image_type, width, height, x_dpi, y_dpi) = _process_gif(data) 

1013 

1014 else: 

1015 raise UnsupportedImageFormat( 

1016 "%s: Unknown or unsupported image file format." % filename 

1017 ) 

1018 

1019 # Check that we found the required data. 

1020 if not height or not width: 

1021 raise UndefinedImageSize("%s: no size data found in image file." % filename) 

1022 

1023 if not image_data: 

1024 fh.close() 

1025 

1026 # Set a default dpi for images with 0 dpi. 

1027 if x_dpi == 0: 

1028 x_dpi = 96 

1029 if y_dpi == 0: 

1030 y_dpi = 96 

1031 

1032 return image_type, width, height, image_name, x_dpi, y_dpi, digest 

1033 

1034 

1035def _process_png(data): 

1036 # Extract width and height information from a PNG file. 

1037 offset = 8 

1038 data_length = len(data) 

1039 end_marker = False 

1040 width = 0 

1041 height = 0 

1042 x_dpi = 96 

1043 y_dpi = 96 

1044 

1045 # Search through the image data to read the height and width in the 

1046 # IHDR element. Also read the DPI in the pHYs element. 

1047 while not end_marker and offset < data_length: 

1048 length = unpack(">I", data[offset + 0 : offset + 4])[0] 

1049 marker = unpack("4s", data[offset + 4 : offset + 8])[0] 

1050 

1051 # Read the image dimensions. 

1052 if marker == b"IHDR": 

1053 width = unpack(">I", data[offset + 8 : offset + 12])[0] 

1054 height = unpack(">I", data[offset + 12 : offset + 16])[0] 

1055 

1056 # Read the image DPI. 

1057 if marker == b"pHYs": 

1058 x_density = unpack(">I", data[offset + 8 : offset + 12])[0] 

1059 y_density = unpack(">I", data[offset + 12 : offset + 16])[0] 

1060 units = unpack("b", data[offset + 16 : offset + 17])[0] 

1061 

1062 if units == 1: 

1063 x_dpi = x_density * 0.0254 

1064 y_dpi = y_density * 0.0254 

1065 

1066 if marker == b"IEND": 

1067 end_marker = True 

1068 continue 

1069 

1070 offset = offset + length + 12 

1071 

1072 return "png", width, height, x_dpi, y_dpi 

1073 

1074 

1075def _process_jpg(data): 

1076 # Extract width and height information from a JPEG file. 

1077 offset = 2 

1078 data_length = len(data) 

1079 end_marker = False 

1080 width = 0 

1081 height = 0 

1082 x_dpi = 96 

1083 y_dpi = 96 

1084 

1085 # Search through the image data to read the JPEG markers. 

1086 while not end_marker and offset < data_length: 

1087 marker = unpack(">H", data[offset + 0 : offset + 2])[0] 

1088 length = unpack(">H", data[offset + 2 : offset + 4])[0] 

1089 

1090 # Read the height and width in the 0xFFCn elements (except C4, C8 

1091 # and CC which aren't SOF markers). 

1092 if ( 

1093 (marker & 0xFFF0) == 0xFFC0 

1094 and marker != 0xFFC4 

1095 and marker != 0xFFC8 

1096 and marker != 0xFFCC 

1097 ): 

1098 height = unpack(">H", data[offset + 5 : offset + 7])[0] 

1099 width = unpack(">H", data[offset + 7 : offset + 9])[0] 

1100 

1101 # Read the DPI in the 0xFFE0 element. 

1102 if marker == 0xFFE0: 

1103 units = unpack("b", data[offset + 11 : offset + 12])[0] 

1104 x_density = unpack(">H", data[offset + 12 : offset + 14])[0] 

1105 y_density = unpack(">H", data[offset + 14 : offset + 16])[0] 

1106 

1107 if units == 1: 

1108 x_dpi = x_density 

1109 y_dpi = y_density 

1110 

1111 if units == 2: 

1112 x_dpi = x_density * 2.54 

1113 y_dpi = y_density * 2.54 

1114 

1115 # Workaround for incorrect dpi. 

1116 if x_dpi == 1: 

1117 x_dpi = 96 

1118 if y_dpi == 1: 

1119 y_dpi = 96 

1120 

1121 if marker == 0xFFDA: 

1122 end_marker = True 

1123 continue 

1124 

1125 offset = offset + length + 2 

1126 

1127 return "jpeg", width, height, x_dpi, y_dpi 

1128 

1129 

1130def _process_gif(data): 

1131 # Extract width and height information from a GIF file. 

1132 x_dpi = 96 

1133 y_dpi = 96 

1134 

1135 width = unpack("<h", data[6:8])[0] 

1136 height = unpack("<h", data[8:10])[0] 

1137 

1138 return "gif", width, height, x_dpi, y_dpi 

1139 

1140 

1141def _process_bmp(data): 

1142 # Extract width and height information from a BMP file. 

1143 width = unpack("<L", data[18:22])[0] 

1144 height = unpack("<L", data[22:26])[0] 

1145 return "bmp", width, height 

1146 

1147 

1148def _process_wmf(data): 

1149 # Extract width and height information from a WMF file. 

1150 x_dpi = 96 

1151 y_dpi = 96 

1152 

1153 # Read the bounding box, measured in logical units. 

1154 x1 = unpack("<h", data[6:8])[0] 

1155 y1 = unpack("<h", data[8:10])[0] 

1156 x2 = unpack("<h", data[10:12])[0] 

1157 y2 = unpack("<h", data[12:14])[0] 

1158 

1159 # Read the number of logical units per inch. Used to scale the image. 

1160 inch = unpack("<H", data[14:16])[0] 

1161 

1162 # Convert to rendered height and width. 

1163 width = float((x2 - x1) * x_dpi) / inch 

1164 height = float((y2 - y1) * y_dpi) / inch 

1165 

1166 return "wmf", width, height, x_dpi, y_dpi 

1167 

1168 

1169def _process_emf(data): 

1170 # Extract width and height information from a EMF file. 

1171 

1172 # Read the bounding box, measured in logical units. 

1173 bound_x1 = unpack("<l", data[8:12])[0] 

1174 bound_y1 = unpack("<l", data[12:16])[0] 

1175 bound_x2 = unpack("<l", data[16:20])[0] 

1176 bound_y2 = unpack("<l", data[20:24])[0] 

1177 

1178 # Convert the bounds to width and height. 

1179 width = bound_x2 - bound_x1 

1180 height = bound_y2 - bound_y1 

1181 

1182 # Read the rectangular frame in units of 0.01mm. 

1183 frame_x1 = unpack("<l", data[24:28])[0] 

1184 frame_y1 = unpack("<l", data[28:32])[0] 

1185 frame_x2 = unpack("<l", data[32:36])[0] 

1186 frame_y2 = unpack("<l", data[36:40])[0] 

1187 

1188 # Convert the frame bounds to mm width and height. 

1189 width_mm = 0.01 * (frame_x2 - frame_x1) 

1190 height_mm = 0.01 * (frame_y2 - frame_y1) 

1191 

1192 # Get the dpi based on the logical size. 

1193 x_dpi = width * 25.4 / width_mm 

1194 y_dpi = height * 25.4 / height_mm 

1195 

1196 # This is to match Excel's calculation. It is probably to account for 

1197 # the fact that the bounding box is inclusive-inclusive. Or a bug. 

1198 width += 1 

1199 height += 1 

1200 

1201 return "emf", width, height, x_dpi, y_dpi