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

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

846 statements  

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

2# 

3# Workbook - A class for writing the Excel XLSX Workbook file. 

4# 

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

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

7# 

8 

9# Standard packages. 

10import operator 

11import os 

12import re 

13import time 

14from datetime import datetime, timezone 

15from decimal import Decimal 

16from fractions import Fraction 

17from warnings import warn 

18from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED, LargeZipFile 

19 

20 

21# Package imports. 

22from . import xmlwriter 

23from .worksheet import Worksheet 

24from .chartsheet import Chartsheet 

25from .sharedstrings import SharedStringTable 

26from .format import Format 

27from .packager import Packager 

28from .utility import xl_cell_to_rowcol 

29from .chart_area import ChartArea 

30from .chart_bar import ChartBar 

31from .chart_column import ChartColumn 

32from .chart_doughnut import ChartDoughnut 

33from .chart_line import ChartLine 

34from .chart_pie import ChartPie 

35from .chart_radar import ChartRadar 

36from .chart_scatter import ChartScatter 

37from .chart_stock import ChartStock 

38from .exceptions import InvalidWorksheetName 

39from .exceptions import DuplicateWorksheetName 

40from .exceptions import FileCreateError 

41from .exceptions import FileSizeError 

42from .utility import get_image_properties 

43 

44 

45class Workbook(xmlwriter.XMLwriter): 

46 """ 

47 A class for writing the Excel XLSX Workbook file. 

48 

49 

50 """ 

51 

52 ########################################################################### 

53 # 

54 # Public API. 

55 # 

56 ########################################################################### 

57 chartsheet_class = Chartsheet 

58 worksheet_class = Worksheet 

59 

60 def __init__(self, filename=None, options=None): 

61 """ 

62 Constructor. 

63 

64 """ 

65 if options is None: 

66 options = {} 

67 

68 super(Workbook, self).__init__() 

69 

70 self.filename = filename 

71 

72 self.tmpdir = options.get("tmpdir", None) 

73 self.date_1904 = options.get("date_1904", False) 

74 self.strings_to_numbers = options.get("strings_to_numbers", False) 

75 self.strings_to_formulas = options.get("strings_to_formulas", True) 

76 self.strings_to_urls = options.get("strings_to_urls", True) 

77 self.nan_inf_to_errors = options.get("nan_inf_to_errors", False) 

78 self.default_date_format = options.get("default_date_format", None) 

79 self.constant_memory = options.get("constant_memory", False) 

80 self.in_memory = options.get("in_memory", False) 

81 self.excel2003_style = options.get("excel2003_style", False) 

82 self.remove_timezone = options.get("remove_timezone", False) 

83 self.use_future_functions = options.get("use_future_functions", False) 

84 self.default_format_properties = options.get("default_format_properties", {}) 

85 

86 self.max_url_length = options.get("max_url_length", 2079) 

87 if self.max_url_length < 255: 

88 self.max_url_length = 2079 

89 

90 if options.get("use_zip64"): 

91 self.allow_zip64 = True 

92 else: 

93 self.allow_zip64 = False 

94 

95 self.worksheet_meta = WorksheetMeta() 

96 self.selected = 0 

97 self.fileclosed = 0 

98 self.filehandle = None 

99 self.internal_fh = 0 

100 self.sheet_name = "Sheet" 

101 self.chart_name = "Chart" 

102 self.sheetname_count = 0 

103 self.chartname_count = 0 

104 self.worksheets_objs = [] 

105 self.charts = [] 

106 self.drawings = [] 

107 self.sheetnames = {} 

108 self.formats = [] 

109 self.xf_formats = [] 

110 self.xf_format_indices = {} 

111 self.dxf_formats = [] 

112 self.dxf_format_indices = {} 

113 self.palette = [] 

114 self.font_count = 0 

115 self.num_formats = [] 

116 self.defined_names = [] 

117 self.named_ranges = [] 

118 self.custom_colors = [] 

119 self.doc_properties = {} 

120 self.custom_properties = [] 

121 self.createtime = datetime.now(timezone.utc) 

122 self.num_vml_files = 0 

123 self.num_comment_files = 0 

124 self.x_window = 240 

125 self.y_window = 15 

126 self.window_width = 16095 

127 self.window_height = 9660 

128 self.tab_ratio = 600 

129 self.str_table = SharedStringTable() 

130 self.vba_project = None 

131 self.vba_project_is_stream = False 

132 self.vba_project_signature = None 

133 self.vba_project_signature_is_stream = False 

134 self.vba_codename = None 

135 self.image_types = {} 

136 self.images = [] 

137 self.border_count = 0 

138 self.fill_count = 0 

139 self.drawing_count = 0 

140 self.calc_mode = "auto" 

141 self.calc_on_load = True 

142 self.calc_id = 124519 

143 self.has_comments = False 

144 self.read_only = 0 

145 self.has_metadata = False 

146 self.has_embedded_images = False 

147 self.has_dynamic_functions = False 

148 self.has_embedded_descriptions = False 

149 self.embedded_images = EmbeddedImages() 

150 

151 # We can't do 'constant_memory' mode while doing 'in_memory' mode. 

152 if self.in_memory: 

153 self.constant_memory = False 

154 

155 # Add the default cell format. 

156 if self.excel2003_style: 

157 self.add_format({"xf_index": 0, "font_family": 0}) 

158 else: 

159 self.add_format({"xf_index": 0}) 

160 

161 # Add a default URL format. 

162 self.default_url_format = self.add_format({"hyperlink": True}) 

163 

164 # Add the default date format. 

165 if self.default_date_format is not None: 

166 self.default_date_format = self.add_format( 

167 {"num_format": self.default_date_format} 

168 ) 

169 

170 def __enter__(self): 

171 """Return self object to use with "with" statement.""" 

172 return self 

173 

174 def __exit__(self, type, value, traceback): 

175 """Close workbook when exiting "with" statement.""" 

176 self.close() 

177 

178 def add_worksheet(self, name=None, worksheet_class=None): 

179 """ 

180 Add a new worksheet to the Excel workbook. 

181 

182 Args: 

183 name: The worksheet name. Defaults to 'Sheet1', etc. 

184 

185 Returns: 

186 Reference to a worksheet object. 

187 

188 """ 

189 if worksheet_class is None: 

190 worksheet_class = self.worksheet_class 

191 

192 return self._add_sheet(name, worksheet_class=worksheet_class) 

193 

194 def add_chartsheet(self, name=None, chartsheet_class=None): 

195 """ 

196 Add a new chartsheet to the Excel workbook. 

197 

198 Args: 

199 name: The chartsheet name. Defaults to 'Sheet1', etc. 

200 

201 Returns: 

202 Reference to a chartsheet object. 

203 

204 """ 

205 if chartsheet_class is None: 

206 chartsheet_class = self.chartsheet_class 

207 

208 return self._add_sheet(name, worksheet_class=chartsheet_class) 

209 

210 def add_format(self, properties=None): 

211 """ 

212 Add a new Format to the Excel Workbook. 

213 

214 Args: 

215 properties: The format properties. 

216 

217 Returns: 

218 Reference to a Format object. 

219 

220 """ 

221 format_properties = self.default_format_properties.copy() 

222 

223 if self.excel2003_style: 

224 format_properties = {"font_name": "Arial", "font_size": 10, "theme": 1 * -1} 

225 

226 if properties: 

227 format_properties.update(properties) 

228 

229 xf_format = Format( 

230 format_properties, self.xf_format_indices, self.dxf_format_indices 

231 ) 

232 

233 # Store the format reference. 

234 self.formats.append(xf_format) 

235 

236 return xf_format 

237 

238 def add_chart(self, options): 

239 """ 

240 Create a chart object. 

241 

242 Args: 

243 options: The chart type and subtype options. 

244 

245 Returns: 

246 Reference to a Chart object. 

247 

248 """ 

249 

250 # Type must be specified so we can create the required chart instance. 

251 chart_type = options.get("type") 

252 if chart_type is None: 

253 warn("Chart type must be defined in add_chart()") 

254 return 

255 

256 if chart_type == "area": 

257 chart = ChartArea(options) 

258 elif chart_type == "bar": 

259 chart = ChartBar(options) 

260 elif chart_type == "column": 

261 chart = ChartColumn(options) 

262 elif chart_type == "doughnut": 

263 chart = ChartDoughnut(options) 

264 elif chart_type == "line": 

265 chart = ChartLine(options) 

266 elif chart_type == "pie": 

267 chart = ChartPie(options) 

268 elif chart_type == "radar": 

269 chart = ChartRadar(options) 

270 elif chart_type == "scatter": 

271 chart = ChartScatter(options) 

272 elif chart_type == "stock": 

273 chart = ChartStock(options) 

274 else: 

275 warn("Unknown chart type '%s' in add_chart()" % chart_type) 

276 return 

277 

278 # Set the embedded chart name if present. 

279 if "name" in options: 

280 chart.chart_name = options["name"] 

281 

282 chart.embedded = True 

283 chart.date_1904 = self.date_1904 

284 chart.remove_timezone = self.remove_timezone 

285 

286 self.charts.append(chart) 

287 

288 return chart 

289 

290 def add_vba_project(self, vba_project, is_stream=False): 

291 """ 

292 Add a vbaProject binary to the Excel workbook. 

293 

294 Args: 

295 vba_project: The vbaProject binary file name. 

296 is_stream: vba_project is an in memory byte stream. 

297 

298 Returns: 

299 Nothing. 

300 

301 """ 

302 if not is_stream and not os.path.exists(vba_project): 

303 warn("VBA project binary file '%s' not found." % vba_project) 

304 return -1 

305 

306 if self.vba_codename is None: 

307 self.vba_codename = "ThisWorkbook" 

308 

309 self.vba_project = vba_project 

310 self.vba_project_is_stream = is_stream 

311 

312 def add_signed_vba_project( 

313 self, vba_project, signature, project_is_stream=False, signature_is_stream=False 

314 ): 

315 """ 

316 Add a vbaProject binary and a vbaProjectSignature binary to the 

317 Excel workbook. 

318 

319 Args: 

320 vba_project: The vbaProject binary file name. 

321 signature: The vbaProjectSignature binary file name. 

322 project_is_stream: vba_project is an in memory byte stream. 

323 signature_is_stream: signature is an in memory byte stream. 

324 

325 Returns: 

326 Nothing. 

327 

328 """ 

329 if self.add_vba_project(vba_project, project_is_stream) == -1: 

330 return -1 

331 

332 if not signature_is_stream and not os.path.exists(signature): 

333 warn("VBA project signature binary file '%s' not found." % signature) 

334 return -1 

335 

336 self.vba_project_signature = signature 

337 self.vba_project_signature_is_stream = signature_is_stream 

338 

339 def close(self): 

340 """ 

341 Call finalization code and close file. 

342 

343 Args: 

344 None. 

345 

346 Returns: 

347 Nothing. 

348 

349 """ 

350 if not self.fileclosed: 

351 try: 

352 self._store_workbook() 

353 except IOError as e: 

354 raise FileCreateError(e) 

355 except LargeZipFile: 

356 raise FileSizeError( 

357 "Filesize would require ZIP64 extensions. " 

358 "Use workbook.use_zip64()." 

359 ) 

360 

361 self.fileclosed = True 

362 

363 # Ensure all constant_memory temp files are closed. 

364 if self.constant_memory: 

365 for worksheet in self.worksheets(): 

366 worksheet._opt_close() 

367 

368 else: 

369 warn("Calling close() on already closed file.") 

370 

371 def set_size(self, width, height): 

372 """ 

373 Set the size of a workbook window. 

374 

375 Args: 

376 width: Width of the window in pixels. 

377 height: Height of the window in pixels. 

378 

379 Returns: 

380 Nothing. 

381 

382 """ 

383 # Convert the width/height to twips at 96 dpi. 

384 if width: 

385 self.window_width = int(width * 1440 / 96) 

386 else: 

387 self.window_width = 16095 

388 

389 if height: 

390 self.window_height = int(height * 1440 / 96) 

391 else: 

392 self.window_height = 9660 

393 

394 def set_tab_ratio(self, tab_ratio=None): 

395 """ 

396 Set the ratio between worksheet tabs and the horizontal slider. 

397 

398 Args: 

399 tab_ratio: The tab ratio, 0 <= tab_ratio <= 100 

400 

401 Returns: 

402 Nothing. 

403 

404 """ 

405 if tab_ratio is None: 

406 return 

407 

408 if tab_ratio < 0 or tab_ratio > 100: 

409 warn("Tab ratio '%d' outside: 0 <= tab_ratio <= 100" % tab_ratio) 

410 else: 

411 self.tab_ratio = int(tab_ratio * 10) 

412 

413 def set_properties(self, properties): 

414 """ 

415 Set the document properties such as Title, Author etc. 

416 

417 Args: 

418 properties: Dictionary of document properties. 

419 

420 Returns: 

421 Nothing. 

422 

423 """ 

424 self.doc_properties = properties 

425 

426 def set_custom_property(self, name, value, property_type=None): 

427 """ 

428 Set a custom document property. 

429 

430 Args: 

431 name: The name of the custom property. 

432 value: The value of the custom property. 

433 property_type: The type of the custom property. Optional. 

434 

435 Returns: 

436 Nothing. 

437 

438 """ 

439 if name is None or value is None: 

440 warn( 

441 "The name and value parameters must be non-None in " 

442 "set_custom_property()" 

443 ) 

444 return -1 

445 

446 if property_type is None: 

447 # Determine the property type from the Python type. 

448 if isinstance(value, bool): 

449 property_type = "bool" 

450 elif isinstance(value, datetime): 

451 property_type = "date" 

452 elif isinstance(value, int): 

453 property_type = "number_int" 

454 elif isinstance(value, (float, int, Decimal, Fraction)): 

455 property_type = "number" 

456 else: 

457 property_type = "text" 

458 

459 if property_type == "date": 

460 value = value.strftime("%Y-%m-%dT%H:%M:%SZ") 

461 

462 if property_type == "text" and len(value) > 255: 

463 warn( 

464 "Length of 'value' parameter exceeds Excel's limit of 255 " 

465 "characters in set_custom_property(): '%s'" % value 

466 ) 

467 

468 if len(name) > 255: 

469 warn( 

470 "Length of 'name' parameter exceeds Excel's limit of 255 " 

471 "characters in set_custom_property(): '%s'" % name 

472 ) 

473 

474 self.custom_properties.append((name, value, property_type)) 

475 

476 def set_calc_mode(self, mode, calc_id=None): 

477 """ 

478 Set the Excel calculation mode for the workbook. 

479 

480 Args: 

481 mode: String containing one of: 

482 * manual 

483 * auto_except_tables 

484 * auto 

485 

486 Returns: 

487 Nothing. 

488 

489 """ 

490 self.calc_mode = mode 

491 

492 if mode == "manual": 

493 self.calc_on_load = False 

494 elif mode == "auto_except_tables": 

495 self.calc_mode = "autoNoTable" 

496 

497 # Leave undocumented for now. Rarely required. 

498 if calc_id: 

499 self.calc_id = calc_id 

500 

501 def define_name(self, name, formula): 

502 # Create a defined name in Excel. We handle global/workbook level 

503 # names and local/worksheet names. 

504 """ 

505 Create a defined name in the workbook. 

506 

507 Args: 

508 name: The defined name. 

509 formula: The cell or range that the defined name refers to. 

510 

511 Returns: 

512 Nothing. 

513 

514 """ 

515 sheet_index = None 

516 sheetname = "" 

517 

518 # Remove the = sign from the formula if it exists. 

519 if formula.startswith("="): 

520 formula = formula.lstrip("=") 

521 

522 # Local defined names are formatted like "Sheet1!name". 

523 sheet_parts = re.compile(r"^([^!]+)!([^!]+)$") 

524 match = sheet_parts.match(name) 

525 

526 if match: 

527 sheetname = match.group(1) 

528 name = match.group(2) 

529 sheet_index = self._get_sheet_index(sheetname) 

530 

531 # Warn if the sheet index wasn't found. 

532 if sheet_index is None: 

533 warn("Unknown sheet name '%s' in defined_name()" % sheetname) 

534 return -1 

535 else: 

536 # Use -1 to indicate global names. 

537 sheet_index = -1 

538 

539 # Warn if the defined name contains invalid chars as defined by Excel. 

540 if not re.match(r"^[\w\\][\w\\.]*$", name, re.UNICODE) or re.match( 

541 r"^\d", name 

542 ): 

543 warn("Invalid Excel characters in defined_name(): '%s'" % name) 

544 return -1 

545 

546 # Warn if the defined name looks like a cell name. 

547 if re.match(r"^[a-zA-Z][a-zA-Z]?[a-dA-D]?\d+$", name): 

548 warn("Name looks like a cell name in defined_name(): '%s'" % name) 

549 return -1 

550 

551 # Warn if the name looks like a R1C1 cell reference. 

552 if re.match(r"^[rcRC]$", name) or re.match(r"^[rcRC]\d+[rcRC]\d+$", name): 

553 warn("Invalid name '%s' like a RC cell ref in defined_name()" % name) 

554 return -1 

555 

556 self.defined_names.append([name, sheet_index, formula, False]) 

557 

558 def worksheets(self): 

559 """ 

560 Return a list of the worksheet objects in the workbook. 

561 

562 Args: 

563 None. 

564 

565 Returns: 

566 A list of worksheet objects. 

567 

568 """ 

569 return self.worksheets_objs 

570 

571 def get_worksheet_by_name(self, name): 

572 """ 

573 Return a worksheet object in the workbook using the sheetname. 

574 

575 Args: 

576 name: The name of the worksheet. 

577 

578 Returns: 

579 A worksheet object or None. 

580 

581 """ 

582 return self.sheetnames.get(name) 

583 

584 def get_default_url_format(self): 

585 """ 

586 Get the default url format used when a user defined format isn't 

587 specified with write_url(). The format is the hyperlink style defined 

588 by Excel for the default theme. 

589 

590 Args: 

591 None. 

592 

593 Returns: 

594 A format object. 

595 

596 """ 

597 return self.default_url_format 

598 

599 def use_zip64(self): 

600 """ 

601 Allow ZIP64 extensions when writing xlsx file zip container. 

602 

603 Args: 

604 None. 

605 

606 Returns: 

607 Nothing. 

608 

609 """ 

610 self.allow_zip64 = True 

611 

612 def set_vba_name(self, name=None): 

613 """ 

614 Set the VBA name for the workbook. By default the workbook is referred 

615 to as ThisWorkbook in VBA. 

616 

617 Args: 

618 name: The VBA name for the workbook. 

619 

620 Returns: 

621 Nothing. 

622 

623 """ 

624 if name is not None: 

625 self.vba_codename = name 

626 else: 

627 self.vba_codename = "ThisWorkbook" 

628 

629 def read_only_recommended(self): 

630 """ 

631 Set the Excel "Read-only recommended" option when saving a file. 

632 

633 Args: 

634 None. 

635 

636 Returns: 

637 Nothing. 

638 

639 """ 

640 self.read_only = 2 

641 

642 ########################################################################### 

643 # 

644 # Private API. 

645 # 

646 ########################################################################### 

647 

648 def _assemble_xml_file(self): 

649 # Assemble and write the XML file. 

650 

651 # Prepare format object for passing to Style.pm. 

652 self._prepare_format_properties() 

653 

654 # Write the XML declaration. 

655 self._xml_declaration() 

656 

657 # Write the workbook element. 

658 self._write_workbook() 

659 

660 # Write the fileVersion element. 

661 self._write_file_version() 

662 

663 # Write the fileSharing element. 

664 self._write_file_sharing() 

665 

666 # Write the workbookPr element. 

667 self._write_workbook_pr() 

668 

669 # Write the bookViews element. 

670 self._write_book_views() 

671 

672 # Write the sheets element. 

673 self._write_sheets() 

674 

675 # Write the workbook defined names. 

676 self._write_defined_names() 

677 

678 # Write the calcPr element. 

679 self._write_calc_pr() 

680 

681 # Close the workbook tag. 

682 self._xml_end_tag("workbook") 

683 

684 # Close the file. 

685 self._xml_close() 

686 

687 def _store_workbook(self): 

688 # Create the xlsx/zip file. 

689 try: 

690 xlsx_file = ZipFile( 

691 self.filename, 

692 "w", 

693 compression=ZIP_DEFLATED, 

694 allowZip64=self.allow_zip64, 

695 ) 

696 except IOError as e: 

697 raise e 

698 

699 # Assemble worksheets into a workbook. 

700 packager = self._get_packager() 

701 

702 # Add a default worksheet if non have been added. 

703 if not self.worksheets(): 

704 self.add_worksheet() 

705 

706 # Ensure that at least one worksheet has been selected. 

707 if self.worksheet_meta.activesheet == 0: 

708 self.worksheets_objs[0].selected = 1 

709 self.worksheets_objs[0].hidden = 0 

710 

711 # Set the active sheet. 

712 for sheet in self.worksheets(): 

713 if sheet.index == self.worksheet_meta.activesheet: 

714 sheet.active = 1 

715 

716 # Set the sheet vba_codename the workbook has a vbaProject binary. 

717 if self.vba_project: 

718 for sheet in self.worksheets(): 

719 if sheet.vba_codename is None: 

720 sheet.set_vba_name() 

721 

722 # Convert the SST strings data structure. 

723 self._prepare_sst_string_data() 

724 

725 # Prepare the worksheet VML elements such as comments and buttons. 

726 self._prepare_vml() 

727 

728 # Set the defined names for the worksheets such as Print Titles. 

729 self._prepare_defined_names() 

730 

731 # Prepare the drawings, charts and images. 

732 self._prepare_drawings() 

733 

734 # Add cached data to charts. 

735 self._add_chart_data() 

736 

737 # Prepare the worksheet tables. 

738 self._prepare_tables() 

739 

740 # Prepare the metadata file links. 

741 self._prepare_metadata() 

742 

743 # Package the workbook. 

744 packager._add_workbook(self) 

745 packager._set_tmpdir(self.tmpdir) 

746 packager._set_in_memory(self.in_memory) 

747 xml_files = packager._create_package() 

748 

749 # Free up the Packager object. 

750 packager = None 

751 

752 # Add XML sub-files to the Zip file with their Excel filename. 

753 for file_id, file_data in enumerate(xml_files): 

754 os_filename, xml_filename, is_binary = file_data 

755 

756 if self.in_memory: 

757 # Set sub-file timestamp to Excel's timestamp of 1/1/1980. 

758 zipinfo = ZipInfo(xml_filename, (1980, 1, 1, 0, 0, 0)) 

759 

760 # Copy compression type from parent ZipFile. 

761 zipinfo.compress_type = xlsx_file.compression 

762 

763 if is_binary: 

764 xlsx_file.writestr(zipinfo, os_filename.getvalue()) 

765 else: 

766 xlsx_file.writestr(zipinfo, os_filename.getvalue().encode("utf-8")) 

767 else: 

768 # The sub-files are tempfiles on disk, i.e, not in memory. 

769 

770 # Set sub-file timestamp to 31/1/1980 due to portability 

771 # issues setting it to Excel's timestamp of 1/1/1980. 

772 timestamp = time.mktime((1980, 1, 31, 0, 0, 0, 0, 0, -1)) 

773 os.utime(os_filename, (timestamp, timestamp)) 

774 

775 try: 

776 xlsx_file.write(os_filename, xml_filename) 

777 os.remove(os_filename) 

778 except LargeZipFile as e: 

779 # Close open temp files on zipfile.LargeZipFile exception. 

780 for i in range(file_id, len(xml_files) - 1): 

781 os.remove(xml_files[i][0]) 

782 raise e 

783 

784 xlsx_file.close() 

785 

786 def _add_sheet(self, name, worksheet_class=None): 

787 # Utility for shared code in add_worksheet() and add_chartsheet(). 

788 

789 if worksheet_class: 

790 worksheet = worksheet_class() 

791 else: 

792 worksheet = self.worksheet_class() 

793 

794 sheet_index = len(self.worksheets_objs) 

795 name = self._check_sheetname(name, isinstance(worksheet, Chartsheet)) 

796 

797 # Initialization data to pass to the worksheet. 

798 init_data = { 

799 "name": name, 

800 "index": sheet_index, 

801 "str_table": self.str_table, 

802 "worksheet_meta": self.worksheet_meta, 

803 "constant_memory": self.constant_memory, 

804 "tmpdir": self.tmpdir, 

805 "date_1904": self.date_1904, 

806 "strings_to_numbers": self.strings_to_numbers, 

807 "strings_to_formulas": self.strings_to_formulas, 

808 "strings_to_urls": self.strings_to_urls, 

809 "nan_inf_to_errors": self.nan_inf_to_errors, 

810 "default_date_format": self.default_date_format, 

811 "default_url_format": self.default_url_format, 

812 "excel2003_style": self.excel2003_style, 

813 "remove_timezone": self.remove_timezone, 

814 "max_url_length": self.max_url_length, 

815 "use_future_functions": self.use_future_functions, 

816 "embedded_images": self.embedded_images, 

817 } 

818 

819 worksheet._initialize(init_data) 

820 

821 self.worksheets_objs.append(worksheet) 

822 self.sheetnames[name] = worksheet 

823 

824 return worksheet 

825 

826 def _check_sheetname(self, sheetname, is_chartsheet=False): 

827 # Check for valid worksheet names. We check the length, if it contains 

828 # any invalid chars and if the sheetname is unique in the workbook. 

829 invalid_char = re.compile(r"[\[\]:*?/\\]") 

830 

831 # Increment the Sheet/Chart number used for default sheet names below. 

832 if is_chartsheet: 

833 self.chartname_count += 1 

834 else: 

835 self.sheetname_count += 1 

836 

837 # Supply default Sheet/Chart sheetname if none has been defined. 

838 if sheetname is None or sheetname == "": 

839 if is_chartsheet: 

840 sheetname = self.chart_name + str(self.chartname_count) 

841 else: 

842 sheetname = self.sheet_name + str(self.sheetname_count) 

843 

844 # Check that sheet sheetname is <= 31. Excel limit. 

845 if len(sheetname) > 31: 

846 raise InvalidWorksheetName( 

847 "Excel worksheet name '%s' must be <= 31 chars." % sheetname 

848 ) 

849 

850 # Check that sheetname doesn't contain any invalid characters. 

851 if invalid_char.search(sheetname): 

852 raise InvalidWorksheetName( 

853 "Invalid Excel character '[]:*?/\\' in sheetname '%s'." % sheetname 

854 ) 

855 

856 # Check that sheetname doesn't start or end with an apostrophe. 

857 if sheetname.startswith("'") or sheetname.endswith("'"): 

858 raise InvalidWorksheetName( 

859 'Sheet name cannot start or end with an apostrophe "%s".' % sheetname 

860 ) 

861 

862 # Check that the worksheet name doesn't already exist since this is a 

863 # fatal Excel error. The check must be case insensitive like Excel. 

864 for worksheet in self.worksheets(): 

865 if sheetname.lower() == worksheet.name.lower(): 

866 raise DuplicateWorksheetName( 

867 "Sheetname '%s', with case ignored, is already in use." % sheetname 

868 ) 

869 

870 return sheetname 

871 

872 def _prepare_format_properties(self): 

873 # Prepare all Format properties prior to passing them to styles.py. 

874 

875 # Separate format objects into XF and DXF formats. 

876 self._prepare_formats() 

877 

878 # Set the font index for the format objects. 

879 self._prepare_fonts() 

880 

881 # Set the number format index for the format objects. 

882 self._prepare_num_formats() 

883 

884 # Set the border index for the format objects. 

885 self._prepare_borders() 

886 

887 # Set the fill index for the format objects. 

888 self._prepare_fills() 

889 

890 def _prepare_formats(self): 

891 # Iterate through the XF Format objects and separate them into 

892 # XF and DXF formats. The XF and DF formats then need to be sorted 

893 # back into index order rather than creation order. 

894 xf_formats = [] 

895 dxf_formats = [] 

896 

897 # Sort into XF and DXF formats. 

898 for xf_format in self.formats: 

899 if xf_format.xf_index is not None: 

900 xf_formats.append(xf_format) 

901 

902 if xf_format.dxf_index is not None: 

903 dxf_formats.append(xf_format) 

904 

905 # Pre-extend the format lists. 

906 self.xf_formats = [None] * len(xf_formats) 

907 self.dxf_formats = [None] * len(dxf_formats) 

908 

909 # Rearrange formats into index order. 

910 for xf_format in xf_formats: 

911 index = xf_format.xf_index 

912 self.xf_formats[index] = xf_format 

913 

914 for dxf_format in dxf_formats: 

915 index = dxf_format.dxf_index 

916 self.dxf_formats[index] = dxf_format 

917 

918 def _set_default_xf_indices(self): 

919 # Set the default index for each format. Only used for testing. 

920 

921 formats = list(self.formats) 

922 

923 # Delete the default url format. 

924 del formats[1] 

925 

926 # Skip the default date format if set. 

927 if self.default_date_format is not None: 

928 del formats[1] 

929 

930 # Set the remaining formats. 

931 for xf_format in formats: 

932 xf_format._get_xf_index() 

933 

934 def _prepare_fonts(self): 

935 # Iterate through the XF Format objects and give them an index to 

936 # non-default font elements. 

937 fonts = {} 

938 index = 0 

939 

940 for xf_format in self.xf_formats: 

941 key = xf_format._get_font_key() 

942 if key in fonts: 

943 # Font has already been used. 

944 xf_format.font_index = fonts[key] 

945 xf_format.has_font = 0 

946 else: 

947 # This is a new font. 

948 fonts[key] = index 

949 xf_format.font_index = index 

950 xf_format.has_font = 1 

951 index += 1 

952 

953 self.font_count = index 

954 

955 # For DXF formats we only need to check if the properties have changed. 

956 for xf_format in self.dxf_formats: 

957 # The only font properties that can change for a DXF format are: 

958 # color, bold, italic, underline and strikethrough. 

959 if ( 

960 xf_format.font_color 

961 or xf_format.bold 

962 or xf_format.italic 

963 or xf_format.underline 

964 or xf_format.font_strikeout 

965 ): 

966 xf_format.has_dxf_font = 1 

967 

968 def _prepare_num_formats(self): 

969 # User defined records in Excel start from index 0xA4. 

970 unique_num_formats = {} 

971 num_formats = [] 

972 index = 164 

973 

974 for xf_format in self.xf_formats + self.dxf_formats: 

975 num_format = xf_format.num_format 

976 

977 # Check if num_format is an index to a built-in number format. 

978 if not isinstance(num_format, str): 

979 num_format = int(num_format) 

980 

981 # Number format '0' is indexed as 1 in Excel. 

982 if num_format == 0: 

983 num_format = 1 

984 

985 xf_format.num_format_index = num_format 

986 continue 

987 elif num_format == "0": 

988 # Number format '0' is indexed as 1 in Excel. 

989 xf_format.num_format_index = 1 

990 continue 

991 elif num_format == "General": 

992 # The 'General' format has an number format index of 0. 

993 xf_format.num_format_index = 0 

994 continue 

995 

996 if num_format in unique_num_formats: 

997 # Number xf_format has already been used. 

998 xf_format.num_format_index = unique_num_formats[num_format] 

999 else: 

1000 # Add a new number xf_format. 

1001 unique_num_formats[num_format] = index 

1002 xf_format.num_format_index = index 

1003 index += 1 

1004 

1005 # Only increase font count for XF formats (not DXF formats). 

1006 if xf_format.xf_index: 

1007 num_formats.append(num_format) 

1008 

1009 self.num_formats = num_formats 

1010 

1011 def _prepare_borders(self): 

1012 # Iterate through the XF Format objects and give them an index to 

1013 # non-default border elements. 

1014 borders = {} 

1015 index = 0 

1016 

1017 for xf_format in self.xf_formats: 

1018 key = xf_format._get_border_key() 

1019 

1020 if key in borders: 

1021 # Border has already been used. 

1022 xf_format.border_index = borders[key] 

1023 xf_format.has_border = 0 

1024 else: 

1025 # This is a new border. 

1026 borders[key] = index 

1027 xf_format.border_index = index 

1028 xf_format.has_border = 1 

1029 index += 1 

1030 

1031 self.border_count = index 

1032 

1033 # For DXF formats we only need to check if the properties have changed. 

1034 has_border = re.compile(r"[^0:]") 

1035 

1036 for xf_format in self.dxf_formats: 

1037 key = xf_format._get_border_key() 

1038 

1039 if has_border.search(key): 

1040 xf_format.has_dxf_border = 1 

1041 

1042 def _prepare_fills(self): 

1043 # Iterate through the XF Format objects and give them an index to 

1044 # non-default fill elements. 

1045 # The user defined fill properties start from 2 since there are 2 

1046 # default fills: patternType="none" and patternType="gray125". 

1047 fills = {} 

1048 index = 2 # Start from 2. See above. 

1049 

1050 # Add the default fills. 

1051 fills["0:0:0"] = 0 

1052 fills["17:0:0"] = 1 

1053 

1054 # Store the DXF colors separately since them may be reversed below. 

1055 for xf_format in self.dxf_formats: 

1056 if xf_format.pattern or xf_format.bg_color or xf_format.fg_color: 

1057 xf_format.has_dxf_fill = 1 

1058 xf_format.dxf_bg_color = xf_format.bg_color 

1059 xf_format.dxf_fg_color = xf_format.fg_color 

1060 

1061 for xf_format in self.xf_formats: 

1062 # The following logical statements jointly take care of special 

1063 # cases in relation to cell colors and patterns: 

1064 # 1. For a solid fill (_pattern == 1) Excel reverses the role of 

1065 # foreground and background colors, and 

1066 # 2. If the user specifies a foreground or background color 

1067 # without a pattern they probably wanted a solid fill, so we fill 

1068 # in the defaults. 

1069 if ( 

1070 xf_format.pattern == 1 

1071 and xf_format.bg_color != 0 

1072 and xf_format.fg_color != 0 

1073 ): 

1074 tmp = xf_format.fg_color 

1075 xf_format.fg_color = xf_format.bg_color 

1076 xf_format.bg_color = tmp 

1077 

1078 if ( 

1079 xf_format.pattern <= 1 

1080 and xf_format.bg_color != 0 

1081 and xf_format.fg_color == 0 

1082 ): 

1083 xf_format.fg_color = xf_format.bg_color 

1084 xf_format.bg_color = 0 

1085 xf_format.pattern = 1 

1086 

1087 if ( 

1088 xf_format.pattern <= 1 

1089 and xf_format.bg_color == 0 

1090 and xf_format.fg_color != 0 

1091 ): 

1092 xf_format.pattern = 1 

1093 

1094 key = xf_format._get_fill_key() 

1095 

1096 if key in fills: 

1097 # Fill has already been used. 

1098 xf_format.fill_index = fills[key] 

1099 xf_format.has_fill = 0 

1100 else: 

1101 # This is a new fill. 

1102 fills[key] = index 

1103 xf_format.fill_index = index 

1104 xf_format.has_fill = 1 

1105 index += 1 

1106 

1107 self.fill_count = index 

1108 

1109 def _prepare_defined_names(self): 

1110 # Iterate through the worksheets and store any defined names in 

1111 # addition to any user defined names. Stores the defined names 

1112 # for the Workbook.xml and the named ranges for App.xml. 

1113 defined_names = self.defined_names 

1114 

1115 for sheet in self.worksheets(): 

1116 # Check for Print Area settings. 

1117 if sheet.autofilter_area: 

1118 hidden = 1 

1119 sheet_range = sheet.autofilter_area 

1120 # Store the defined names. 

1121 defined_names.append( 

1122 ["_xlnm._FilterDatabase", sheet.index, sheet_range, hidden] 

1123 ) 

1124 

1125 # Check for Print Area settings. 

1126 if sheet.print_area_range: 

1127 hidden = 0 

1128 sheet_range = sheet.print_area_range 

1129 # Store the defined names. 

1130 defined_names.append( 

1131 ["_xlnm.Print_Area", sheet.index, sheet_range, hidden] 

1132 ) 

1133 

1134 # Check for repeat rows/cols referred to as Print Titles. 

1135 if sheet.repeat_col_range or sheet.repeat_row_range: 

1136 hidden = 0 

1137 sheet_range = "" 

1138 if sheet.repeat_col_range and sheet.repeat_row_range: 

1139 sheet_range = sheet.repeat_col_range + "," + sheet.repeat_row_range 

1140 else: 

1141 sheet_range = sheet.repeat_col_range + sheet.repeat_row_range 

1142 # Store the defined names. 

1143 defined_names.append( 

1144 ["_xlnm.Print_Titles", sheet.index, sheet_range, hidden] 

1145 ) 

1146 

1147 defined_names = self._sort_defined_names(defined_names) 

1148 self.defined_names = defined_names 

1149 self.named_ranges = self._extract_named_ranges(defined_names) 

1150 

1151 def _sort_defined_names(self, names): 

1152 # Sort the list of list of internal and user defined names in 

1153 # the same order as used by Excel. 

1154 

1155 # Add a normalize name string to each list for sorting. 

1156 for name_list in names: 

1157 (defined_name, _, sheet_name, _) = name_list 

1158 

1159 # Normalize the defined name by removing any leading '_xmln.' 

1160 # from internal names and lowercasing the string. 

1161 defined_name = defined_name.replace("_xlnm.", "").lower() 

1162 

1163 # Normalize the sheetname by removing the leading quote and 

1164 # lowercasing the string. 

1165 sheet_name = sheet_name.lstrip("'").lower() 

1166 

1167 name_list.append(defined_name + "::" + sheet_name) 

1168 

1169 # Sort based on the normalized key. 

1170 names.sort(key=operator.itemgetter(4)) 

1171 

1172 # Remove the extra key used for sorting. 

1173 for name_list in names: 

1174 name_list.pop() 

1175 

1176 return names 

1177 

1178 def _prepare_drawings(self): 

1179 # Iterate through the worksheets and set up chart and image drawings. 

1180 chart_ref_id = 0 

1181 ref_id = 0 

1182 drawing_id = 0 

1183 image_ids = {} 

1184 header_image_ids = {} 

1185 background_ids = {} 

1186 

1187 # Store the image types for any embedded images. 

1188 for image_data in self.embedded_images.images: 

1189 image_type = image_data[1] 

1190 self.image_types[image_type] = True 

1191 if image_data[3]: 

1192 self.has_embedded_descriptions = True 

1193 

1194 image_ref_id = len(self.embedded_images.images) 

1195 

1196 for sheet in self.worksheets(): 

1197 chart_count = len(sheet.charts) 

1198 image_count = len(sheet.images) 

1199 shape_count = len(sheet.shapes) 

1200 

1201 header_image_count = len(sheet.header_images) 

1202 footer_image_count = len(sheet.footer_images) 

1203 has_background = sheet.background_image 

1204 has_drawing = False 

1205 

1206 if not ( 

1207 chart_count 

1208 or image_count 

1209 or shape_count 

1210 or header_image_count 

1211 or footer_image_count 

1212 or has_background 

1213 ): 

1214 continue 

1215 

1216 # Don't increase the drawing_id header/footer images. 

1217 if chart_count or image_count or shape_count: 

1218 drawing_id += 1 

1219 has_drawing = True 

1220 

1221 # Prepare the background images. 

1222 if sheet.background_image: 

1223 if sheet.background_bytes: 

1224 filename = "" 

1225 image_data = sheet.background_image 

1226 else: 

1227 filename = sheet.background_image 

1228 image_data = None 

1229 

1230 ( 

1231 image_type, 

1232 _, 

1233 _, 

1234 _, 

1235 _, 

1236 _, 

1237 digest, 

1238 ) = get_image_properties(filename, image_data) 

1239 

1240 self.image_types[image_type] = True 

1241 

1242 if digest in background_ids: 

1243 ref_id = background_ids[digest] 

1244 else: 

1245 image_ref_id += 1 

1246 ref_id = image_ref_id 

1247 background_ids[digest] = image_ref_id 

1248 self.images.append([filename, image_type, image_data]) 

1249 

1250 sheet._prepare_background(ref_id, image_type) 

1251 

1252 # Prepare the worksheet images. 

1253 for index in range(image_count): 

1254 filename = sheet.images[index][2] 

1255 image_data = sheet.images[index][10] 

1256 ( 

1257 image_type, 

1258 width, 

1259 height, 

1260 name, 

1261 x_dpi, 

1262 y_dpi, 

1263 digest, 

1264 ) = get_image_properties(filename, image_data) 

1265 

1266 self.image_types[image_type] = True 

1267 

1268 if digest in image_ids: 

1269 ref_id = image_ids[digest] 

1270 else: 

1271 image_ref_id += 1 

1272 ref_id = image_ref_id 

1273 image_ids[digest] = image_ref_id 

1274 self.images.append([filename, image_type, image_data]) 

1275 

1276 sheet._prepare_image( 

1277 index, 

1278 ref_id, 

1279 drawing_id, 

1280 width, 

1281 height, 

1282 name, 

1283 image_type, 

1284 x_dpi, 

1285 y_dpi, 

1286 digest, 

1287 ) 

1288 

1289 # Prepare the worksheet charts. 

1290 for index in range(chart_count): 

1291 chart_ref_id += 1 

1292 sheet._prepare_chart(index, chart_ref_id, drawing_id) 

1293 

1294 # Prepare the worksheet shapes. 

1295 for index in range(shape_count): 

1296 sheet._prepare_shape(index, drawing_id) 

1297 

1298 # Prepare the header images. 

1299 for index in range(header_image_count): 

1300 filename = sheet.header_images[index][0] 

1301 image_data = sheet.header_images[index][1] 

1302 position = sheet.header_images[index][2] 

1303 

1304 ( 

1305 image_type, 

1306 width, 

1307 height, 

1308 name, 

1309 x_dpi, 

1310 y_dpi, 

1311 digest, 

1312 ) = get_image_properties(filename, image_data) 

1313 

1314 self.image_types[image_type] = True 

1315 

1316 if digest in header_image_ids: 

1317 ref_id = header_image_ids[digest] 

1318 else: 

1319 image_ref_id += 1 

1320 ref_id = image_ref_id 

1321 header_image_ids[digest] = image_ref_id 

1322 self.images.append([filename, image_type, image_data]) 

1323 

1324 sheet._prepare_header_image( 

1325 ref_id, 

1326 width, 

1327 height, 

1328 name, 

1329 image_type, 

1330 position, 

1331 x_dpi, 

1332 y_dpi, 

1333 digest, 

1334 ) 

1335 

1336 # Prepare the footer images. 

1337 for index in range(footer_image_count): 

1338 filename = sheet.footer_images[index][0] 

1339 image_data = sheet.footer_images[index][1] 

1340 position = sheet.footer_images[index][2] 

1341 

1342 ( 

1343 image_type, 

1344 width, 

1345 height, 

1346 name, 

1347 x_dpi, 

1348 y_dpi, 

1349 digest, 

1350 ) = get_image_properties(filename, image_data) 

1351 

1352 self.image_types[image_type] = True 

1353 

1354 if digest in header_image_ids: 

1355 ref_id = header_image_ids[digest] 

1356 else: 

1357 image_ref_id += 1 

1358 ref_id = image_ref_id 

1359 header_image_ids[digest] = image_ref_id 

1360 self.images.append([filename, image_type, image_data]) 

1361 

1362 sheet._prepare_header_image( 

1363 ref_id, 

1364 width, 

1365 height, 

1366 name, 

1367 image_type, 

1368 position, 

1369 x_dpi, 

1370 y_dpi, 

1371 digest, 

1372 ) 

1373 

1374 if has_drawing: 

1375 drawing = sheet.drawing 

1376 self.drawings.append(drawing) 

1377 

1378 # Remove charts that were created but not inserted into worksheets. 

1379 for chart in self.charts[:]: 

1380 if chart.id == -1: 

1381 self.charts.remove(chart) 

1382 

1383 # Sort the workbook charts references into the order that the were 

1384 # written to the worksheets above. 

1385 self.charts = sorted(self.charts, key=lambda chart: chart.id) 

1386 

1387 self.drawing_count = drawing_id 

1388 

1389 def _extract_named_ranges(self, defined_names): 

1390 # Extract the named ranges from the sorted list of defined names. 

1391 # These are used in the App.xml file. 

1392 named_ranges = [] 

1393 

1394 for defined_name in defined_names: 

1395 name = defined_name[0] 

1396 index = defined_name[1] 

1397 sheet_range = defined_name[2] 

1398 

1399 # Skip autoFilter ranges. 

1400 if name == "_xlnm._FilterDatabase": 

1401 continue 

1402 

1403 # We are only interested in defined names with ranges. 

1404 if "!" in sheet_range: 

1405 sheet_name, _ = sheet_range.split("!", 1) 

1406 

1407 # Match Print_Area and Print_Titles xlnm types. 

1408 if name.startswith("_xlnm."): 

1409 xlnm_type = name.replace("_xlnm.", "") 

1410 name = sheet_name + "!" + xlnm_type 

1411 elif index != -1: 

1412 name = sheet_name + "!" + name 

1413 

1414 named_ranges.append(name) 

1415 

1416 return named_ranges 

1417 

1418 def _get_sheet_index(self, sheetname): 

1419 # Convert a sheet name to its index. Return None otherwise. 

1420 sheetname = sheetname.strip("'") 

1421 

1422 if sheetname in self.sheetnames: 

1423 return self.sheetnames[sheetname].index 

1424 else: 

1425 return None 

1426 

1427 def _prepare_vml(self): 

1428 # Iterate through the worksheets and set up the VML objects. 

1429 comment_id = 0 

1430 vml_drawing_id = 0 

1431 vml_data_id = 1 

1432 vml_header_id = 0 

1433 vml_shape_id = 1024 

1434 vml_files = 0 

1435 comment_files = 0 

1436 

1437 for sheet in self.worksheets(): 

1438 if not sheet.has_vml and not sheet.has_header_vml: 

1439 continue 

1440 

1441 vml_files += 1 

1442 

1443 if sheet.has_vml: 

1444 if sheet.has_comments: 

1445 comment_files += 1 

1446 comment_id += 1 

1447 self.has_comments = True 

1448 

1449 vml_drawing_id += 1 

1450 

1451 count = sheet._prepare_vml_objects( 

1452 vml_data_id, vml_shape_id, vml_drawing_id, comment_id 

1453 ) 

1454 

1455 # Each VML should start with a shape id incremented by 1024. 

1456 vml_data_id += 1 * int((1024 + count) / 1024) 

1457 vml_shape_id += 1024 * int((1024 + count) / 1024) 

1458 

1459 if sheet.has_header_vml: 

1460 vml_header_id += 1 

1461 vml_drawing_id += 1 

1462 sheet._prepare_header_vml_objects(vml_header_id, vml_drawing_id) 

1463 

1464 self.num_vml_files = vml_files 

1465 self.num_comment_files = comment_files 

1466 

1467 def _prepare_tables(self): 

1468 # Set the table ids for the worksheet tables. 

1469 table_id = 0 

1470 seen = {} 

1471 

1472 for sheet in self.worksheets(): 

1473 table_count = len(sheet.tables) 

1474 

1475 if not table_count: 

1476 continue 

1477 

1478 sheet._prepare_tables(table_id + 1, seen) 

1479 table_id += table_count 

1480 

1481 def _prepare_metadata(self): 

1482 # Set the metadata rel link. 

1483 self.has_embedded_images = self.embedded_images.has_images() 

1484 self.has_metadata = self.has_embedded_images 

1485 

1486 for sheet in self.worksheets(): 

1487 if sheet.has_dynamic_arrays: 

1488 self.has_metadata = True 

1489 self.has_dynamic_functions = True 

1490 

1491 def _add_chart_data(self): 

1492 # Add "cached" data to charts to provide the numCache and strCache 

1493 # data for series and title/axis ranges. 

1494 worksheets = {} 

1495 seen_ranges = {} 

1496 charts = [] 

1497 

1498 # Map worksheet names to worksheet objects. 

1499 for worksheet in self.worksheets(): 

1500 worksheets[worksheet.name] = worksheet 

1501 

1502 # Build a list of the worksheet charts including any combined charts. 

1503 for chart in self.charts: 

1504 charts.append(chart) 

1505 if chart.combined: 

1506 charts.append(chart.combined) 

1507 

1508 for chart in charts: 

1509 for c_range in chart.formula_ids.keys(): 

1510 r_id = chart.formula_ids[c_range] 

1511 

1512 # Skip if the series has user defined data. 

1513 if chart.formula_data[r_id] is not None: 

1514 if c_range not in seen_ranges or seen_ranges[c_range] is None: 

1515 data = chart.formula_data[r_id] 

1516 seen_ranges[c_range] = data 

1517 continue 

1518 

1519 # Check to see if the data is already cached locally. 

1520 if c_range in seen_ranges: 

1521 chart.formula_data[r_id] = seen_ranges[c_range] 

1522 continue 

1523 

1524 # Convert the range formula to a sheet name and cell range. 

1525 (sheetname, cells) = self._get_chart_range(c_range) 

1526 

1527 # Skip if we couldn't parse the formula. 

1528 if sheetname is None: 

1529 continue 

1530 

1531 # Handle non-contiguous ranges like: 

1532 # (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5). 

1533 # We don't try to parse them. We just return an empty list. 

1534 if sheetname.startswith("("): 

1535 chart.formula_data[r_id] = [] 

1536 seen_ranges[c_range] = [] 

1537 continue 

1538 

1539 # Warn if the name is unknown since it indicates a user error 

1540 # in a chart series formula. 

1541 if sheetname not in worksheets: 

1542 warn( 

1543 "Unknown worksheet reference '%s' in range " 

1544 "'%s' passed to add_series()" % (sheetname, c_range) 

1545 ) 

1546 chart.formula_data[r_id] = [] 

1547 seen_ranges[c_range] = [] 

1548 continue 

1549 

1550 # Find the worksheet object based on the sheet name. 

1551 worksheet = worksheets[sheetname] 

1552 

1553 # Get the data from the worksheet table. 

1554 data = worksheet._get_range_data(*cells) 

1555 

1556 # Add the data to the chart. 

1557 chart.formula_data[r_id] = data 

1558 

1559 # Store range data locally to avoid lookup if seen again. 

1560 seen_ranges[c_range] = data 

1561 

1562 def _get_chart_range(self, c_range): 

1563 # Convert a range formula such as Sheet1!$B$1:$B$5 into a sheet name 

1564 # and cell range such as ( 'Sheet1', 0, 1, 4, 1 ). 

1565 

1566 # Split the range formula into sheetname and cells at the last '!'. 

1567 pos = c_range.rfind("!") 

1568 if pos > 0: 

1569 sheetname = c_range[:pos] 

1570 cells = c_range[pos + 1 :] 

1571 else: 

1572 return None, None 

1573 

1574 # Split the cell range into 2 cells or else use single cell for both. 

1575 if cells.find(":") > 0: 

1576 (cell_1, cell_2) = cells.split(":", 1) 

1577 else: 

1578 (cell_1, cell_2) = (cells, cells) 

1579 

1580 # Remove leading/trailing quotes and convert escaped quotes to single. 

1581 sheetname = sheetname.strip("'") 

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

1583 

1584 try: 

1585 # Get the row, col values from the Excel ranges. We do this in a 

1586 # try block for ranges that can't be parsed such as defined names. 

1587 (row_start, col_start) = xl_cell_to_rowcol(cell_1) 

1588 (row_end, col_end) = xl_cell_to_rowcol(cell_2) 

1589 except AttributeError: 

1590 return None, None 

1591 

1592 # We only handle 1D ranges. 

1593 if row_start != row_end and col_start != col_end: 

1594 return None, None 

1595 

1596 return sheetname, [row_start, col_start, row_end, col_end] 

1597 

1598 def _prepare_sst_string_data(self): 

1599 # Convert the SST string data from a dict to a list. 

1600 self.str_table._sort_string_data() 

1601 

1602 def _get_packager(self): 

1603 # Get and instance of the Packager class to create the xlsx package. 

1604 # This allows the default packager to be over-ridden. 

1605 return Packager() 

1606 

1607 ########################################################################### 

1608 # 

1609 # XML methods. 

1610 # 

1611 ########################################################################### 

1612 

1613 def _write_workbook(self): 

1614 # Write <workbook> element. 

1615 

1616 schema = "http://schemas.openxmlformats.org" 

1617 xmlns = schema + "/spreadsheetml/2006/main" 

1618 xmlns_r = schema + "/officeDocument/2006/relationships" 

1619 

1620 attributes = [ 

1621 ("xmlns", xmlns), 

1622 ("xmlns:r", xmlns_r), 

1623 ] 

1624 

1625 self._xml_start_tag("workbook", attributes) 

1626 

1627 def _write_file_version(self): 

1628 # Write the <fileVersion> element. 

1629 

1630 app_name = "xl" 

1631 last_edited = 4 

1632 lowest_edited = 4 

1633 rup_build = 4505 

1634 

1635 attributes = [ 

1636 ("appName", app_name), 

1637 ("lastEdited", last_edited), 

1638 ("lowestEdited", lowest_edited), 

1639 ("rupBuild", rup_build), 

1640 ] 

1641 

1642 if self.vba_project: 

1643 attributes.append(("codeName", "{37E998C4-C9E5-D4B9-71C8-EB1FF731991C}")) 

1644 

1645 self._xml_empty_tag("fileVersion", attributes) 

1646 

1647 def _write_file_sharing(self): 

1648 # Write the <fileSharing> element. 

1649 if self.read_only == 0: 

1650 return 

1651 

1652 attributes = [("readOnlyRecommended", 1)] 

1653 

1654 self._xml_empty_tag("fileSharing", attributes) 

1655 

1656 def _write_workbook_pr(self): 

1657 # Write <workbookPr> element. 

1658 default_theme_version = 124226 

1659 attributes = [] 

1660 

1661 if self.vba_codename: 

1662 attributes.append(("codeName", self.vba_codename)) 

1663 if self.date_1904: 

1664 attributes.append(("date1904", 1)) 

1665 

1666 attributes.append(("defaultThemeVersion", default_theme_version)) 

1667 

1668 self._xml_empty_tag("workbookPr", attributes) 

1669 

1670 def _write_book_views(self): 

1671 # Write <bookViews> element. 

1672 self._xml_start_tag("bookViews") 

1673 self._write_workbook_view() 

1674 self._xml_end_tag("bookViews") 

1675 

1676 def _write_workbook_view(self): 

1677 # Write <workbookView> element. 

1678 attributes = [ 

1679 ("xWindow", self.x_window), 

1680 ("yWindow", self.y_window), 

1681 ("windowWidth", self.window_width), 

1682 ("windowHeight", self.window_height), 

1683 ] 

1684 

1685 # Store the tabRatio attribute when it isn't the default. 

1686 if self.tab_ratio != 600: 

1687 attributes.append(("tabRatio", self.tab_ratio)) 

1688 

1689 # Store the firstSheet attribute when it isn't the default. 

1690 if self.worksheet_meta.firstsheet > 0: 

1691 firstsheet = self.worksheet_meta.firstsheet + 1 

1692 attributes.append(("firstSheet", firstsheet)) 

1693 

1694 # Store the activeTab attribute when it isn't the first sheet. 

1695 if self.worksheet_meta.activesheet > 0: 

1696 attributes.append(("activeTab", self.worksheet_meta.activesheet)) 

1697 

1698 self._xml_empty_tag("workbookView", attributes) 

1699 

1700 def _write_sheets(self): 

1701 # Write <sheets> element. 

1702 self._xml_start_tag("sheets") 

1703 

1704 id_num = 1 

1705 for worksheet in self.worksheets(): 

1706 self._write_sheet(worksheet.name, id_num, worksheet.hidden) 

1707 id_num += 1 

1708 

1709 self._xml_end_tag("sheets") 

1710 

1711 def _write_sheet(self, name, sheet_id, hidden): 

1712 # Write <sheet> element. 

1713 attributes = [ 

1714 ("name", name), 

1715 ("sheetId", sheet_id), 

1716 ] 

1717 

1718 if hidden == 1: 

1719 attributes.append(("state", "hidden")) 

1720 elif hidden == 2: 

1721 attributes.append(("state", "veryHidden")) 

1722 

1723 attributes.append(("r:id", "rId" + str(sheet_id))) 

1724 

1725 self._xml_empty_tag("sheet", attributes) 

1726 

1727 def _write_calc_pr(self): 

1728 # Write the <calcPr> element. 

1729 attributes = [("calcId", self.calc_id)] 

1730 

1731 if self.calc_mode == "manual": 

1732 attributes.append(("calcMode", self.calc_mode)) 

1733 attributes.append(("calcOnSave", "0")) 

1734 elif self.calc_mode == "autoNoTable": 

1735 attributes.append(("calcMode", self.calc_mode)) 

1736 

1737 if self.calc_on_load: 

1738 attributes.append(("fullCalcOnLoad", "1")) 

1739 

1740 self._xml_empty_tag("calcPr", attributes) 

1741 

1742 def _write_defined_names(self): 

1743 # Write the <definedNames> element. 

1744 if not self.defined_names: 

1745 return 

1746 

1747 self._xml_start_tag("definedNames") 

1748 

1749 for defined_name in self.defined_names: 

1750 self._write_defined_name(defined_name) 

1751 

1752 self._xml_end_tag("definedNames") 

1753 

1754 def _write_defined_name(self, defined_name): 

1755 # Write the <definedName> element. 

1756 name = defined_name[0] 

1757 sheet_id = defined_name[1] 

1758 sheet_range = defined_name[2] 

1759 hidden = defined_name[3] 

1760 

1761 attributes = [("name", name)] 

1762 

1763 if sheet_id != -1: 

1764 attributes.append(("localSheetId", sheet_id)) 

1765 if hidden: 

1766 attributes.append(("hidden", 1)) 

1767 

1768 self._xml_data_element("definedName", sheet_range, attributes) 

1769 

1770 

1771# A metadata class to share data between worksheets. 

1772class WorksheetMeta(object): 

1773 """ 

1774 A class to track worksheets data such as the active sheet and the 

1775 first sheet. 

1776 

1777 """ 

1778 

1779 def __init__(self): 

1780 self.activesheet = 0 

1781 self.firstsheet = 0 

1782 

1783 

1784# A helper class to share embedded images between worksheets. 

1785class EmbeddedImages(object): 

1786 """ 

1787 A class to track duplicate embedded images between worksheets. 

1788 

1789 """ 

1790 

1791 def __init__(self): 

1792 self.images = [] 

1793 self.image_indexes = {} 

1794 

1795 def get_image_index(self, image, digest): 

1796 image_index = self.image_indexes.get(digest) 

1797 

1798 if image_index is None: 

1799 self.images.append(image) 

1800 image_index = len(self.images) 

1801 self.image_indexes[digest] = image_index 

1802 

1803 return image_index 

1804 

1805 def has_images(self): 

1806 return len(self.images) > 0