Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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

854 statements  

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

2# 

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

4# 

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

6# 

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

8# 

9 

10# Standard packages. 

11import operator 

12import os 

13import re 

14import time 

15from datetime import datetime, timezone 

16from decimal import Decimal 

17from fractions import Fraction 

18from warnings import warn 

19from zipfile import ZIP_DEFLATED, LargeZipFile, ZipFile, ZipInfo 

20 

21from xlsxwriter.image import Image 

22 

23# Package imports. 

24from . import xmlwriter 

25from .chart_area import ChartArea 

26from .chart_bar import ChartBar 

27from .chart_column import ChartColumn 

28from .chart_doughnut import ChartDoughnut 

29from .chart_line import ChartLine 

30from .chart_pie import ChartPie 

31from .chart_radar import ChartRadar 

32from .chart_scatter import ChartScatter 

33from .chart_stock import ChartStock 

34from .chartsheet import Chartsheet 

35from .exceptions import ( 

36 DuplicateWorksheetName, 

37 FileCreateError, 

38 FileSizeError, 

39 InvalidWorksheetName, 

40) 

41from .format import Format 

42from .packager import Packager 

43from .sharedstrings import SharedStringTable 

44from .utility import xl_cell_to_rowcol 

45from .worksheet import Worksheet 

46 

47 

48class Workbook(xmlwriter.XMLwriter): 

49 """ 

50 A class for writing the Excel XLSX Workbook file. 

51 

52 

53 """ 

54 

55 ########################################################################### 

56 # 

57 # Public API. 

58 # 

59 ########################################################################### 

60 chartsheet_class = Chartsheet 

61 worksheet_class = Worksheet 

62 

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

64 """ 

65 Constructor. 

66 

67 """ 

68 if options is None: 

69 options = {} 

70 

71 super().__init__() 

72 

73 self.filename = filename 

74 

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

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

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

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

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

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

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

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

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

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

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

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

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

88 

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

90 if self.max_url_length < 255: 

91 self.max_url_length = 2079 

92 

93 if options.get("use_zip64"): 

94 self.allow_zip64 = True 

95 else: 

96 self.allow_zip64 = False 

97 

98 self.worksheet_meta = WorksheetMeta() 

99 self.selected = 0 

100 self.fileclosed = 0 

101 self.filehandle = None 

102 self.internal_fh = 0 

103 self.sheet_name = "Sheet" 

104 self.chart_name = "Chart" 

105 self.sheetname_count = 0 

106 self.chartname_count = 0 

107 self.worksheets_objs = [] 

108 self.charts = [] 

109 self.drawings = [] 

110 self.sheetnames = {} 

111 self.formats = [] 

112 self.xf_formats = [] 

113 self.xf_format_indices = {} 

114 self.dxf_formats = [] 

115 self.dxf_format_indices = {} 

116 self.palette = [] 

117 self.font_count = 0 

118 self.num_formats = [] 

119 self.defined_names = [] 

120 self.named_ranges = [] 

121 self.custom_colors = [] 

122 self.doc_properties = {} 

123 self.custom_properties = [] 

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

125 self.num_vml_files = 0 

126 self.num_comment_files = 0 

127 self.x_window = 240 

128 self.y_window = 15 

129 self.window_width = 16095 

130 self.window_height = 9660 

131 self.tab_ratio = 600 

132 self.str_table = SharedStringTable() 

133 self.vba_project = None 

134 self.vba_project_is_stream = False 

135 self.vba_project_signature = None 

136 self.vba_project_signature_is_stream = False 

137 self.vba_codename = None 

138 self.image_types = {} 

139 self.images = [] 

140 self.border_count = 0 

141 self.fill_count = 0 

142 self.drawing_count = 0 

143 self.calc_mode = "auto" 

144 self.calc_on_load = True 

145 self.calc_id = 124519 

146 self.has_comments = False 

147 self.read_only = 0 

148 self.has_metadata = False 

149 self.has_embedded_images = False 

150 self.has_dynamic_functions = False 

151 self.has_embedded_descriptions = False 

152 self.embedded_images = EmbeddedImages() 

153 self.feature_property_bags = set() 

154 

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

156 if self.in_memory: 

157 self.constant_memory = False 

158 

159 # Add the default cell format. 

160 if self.excel2003_style: 

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

162 else: 

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

164 

165 # Add a default URL format. 

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

167 

168 # Add the default date format. 

169 if self.default_date_format is not None: 

170 self.default_date_format = self.add_format( 

171 {"num_format": self.default_date_format} 

172 ) 

173 

174 def __enter__(self): 

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

176 return self 

177 

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

179 # pylint: disable=redefined-builtin 

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

181 self.close() 

182 

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

184 """ 

185 Add a new worksheet to the Excel workbook. 

186 

187 Args: 

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

189 

190 Returns: 

191 Reference to a worksheet object. 

192 

193 """ 

194 if worksheet_class is None: 

195 worksheet_class = self.worksheet_class 

196 

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

198 

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

200 """ 

201 Add a new chartsheet to the Excel workbook. 

202 

203 Args: 

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

205 

206 Returns: 

207 Reference to a chartsheet object. 

208 

209 """ 

210 if chartsheet_class is None: 

211 chartsheet_class = self.chartsheet_class 

212 

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

214 

215 def add_format(self, properties=None): 

216 """ 

217 Add a new Format to the Excel Workbook. 

218 

219 Args: 

220 properties: The format properties. 

221 

222 Returns: 

223 Reference to a Format object. 

224 

225 """ 

226 format_properties = self.default_format_properties.copy() 

227 

228 if self.excel2003_style: 

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

230 

231 if properties: 

232 format_properties.update(properties) 

233 

234 xf_format = Format( 

235 format_properties, self.xf_format_indices, self.dxf_format_indices 

236 ) 

237 

238 # Store the format reference. 

239 self.formats.append(xf_format) 

240 

241 return xf_format 

242 

243 def add_chart(self, options): 

244 """ 

245 Create a chart object. 

246 

247 Args: 

248 options: The chart type and subtype options. 

249 

250 Returns: 

251 Reference to a Chart object. 

252 

253 """ 

254 

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

256 chart_type = options.get("type") 

257 if chart_type is None: 

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

259 return None 

260 

261 if chart_type == "area": 

262 chart = ChartArea(options) 

263 elif chart_type == "bar": 

264 chart = ChartBar(options) 

265 elif chart_type == "column": 

266 chart = ChartColumn(options) 

267 elif chart_type == "doughnut": 

268 chart = ChartDoughnut() 

269 elif chart_type == "line": 

270 chart = ChartLine(options) 

271 elif chart_type == "pie": 

272 chart = ChartPie() 

273 elif chart_type == "radar": 

274 chart = ChartRadar(options) 

275 elif chart_type == "scatter": 

276 chart = ChartScatter(options) 

277 elif chart_type == "stock": 

278 chart = ChartStock() 

279 else: 

280 warn(f"Unknown chart type '{chart_type}' in add_chart()") 

281 return None 

282 

283 # Set the embedded chart name if present. 

284 if "name" in options: 

285 chart.chart_name = options["name"] 

286 

287 chart.embedded = True 

288 chart.date_1904 = self.date_1904 

289 chart.remove_timezone = self.remove_timezone 

290 

291 self.charts.append(chart) 

292 

293 return chart 

294 

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

296 """ 

297 Add a vbaProject binary to the Excel workbook. 

298 

299 Args: 

300 vba_project: The vbaProject binary file name. 

301 is_stream: vba_project is an in memory byte stream. 

302 

303 Returns: 

304 0 on success. 

305 

306 """ 

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

308 warn(f"VBA project binary file '{vba_project}' not found.") 

309 return -1 

310 

311 if self.vba_codename is None: 

312 self.vba_codename = "ThisWorkbook" 

313 

314 self.vba_project = vba_project 

315 self.vba_project_is_stream = is_stream 

316 

317 return 0 

318 

319 def add_signed_vba_project( 

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

321 ): 

322 """ 

323 Add a vbaProject binary and a vbaProjectSignature binary to the 

324 Excel workbook. 

325 

326 Args: 

327 vba_project: The vbaProject binary file name. 

328 signature: The vbaProjectSignature binary file name. 

329 project_is_stream: vba_project is an in memory byte stream. 

330 signature_is_stream: signature is an in memory byte stream. 

331 

332 Returns: 

333 0 on success. 

334 

335 """ 

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

337 return -1 

338 

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

340 warn(f"VBA project signature binary file '{signature}' not found.") 

341 return -1 

342 

343 self.vba_project_signature = signature 

344 self.vba_project_signature_is_stream = signature_is_stream 

345 

346 return 0 

347 

348 def close(self): 

349 """ 

350 Call finalization code and close file. 

351 

352 Args: 

353 None. 

354 

355 Returns: 

356 Nothing. 

357 

358 """ 

359 # pylint: disable=raise-missing-from 

360 if not self.fileclosed: 

361 try: 

362 self._store_workbook() 

363 except IOError as e: 

364 raise FileCreateError(e) 

365 except LargeZipFile: 

366 raise FileSizeError( 

367 "Filesize would require ZIP64 extensions. " 

368 "Use workbook.use_zip64()." 

369 ) 

370 

371 self.fileclosed = True 

372 

373 # Ensure all constant_memory temp files are closed. 

374 if self.constant_memory: 

375 for worksheet in self.worksheets(): 

376 worksheet._opt_close() 

377 

378 else: 

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

380 

381 def set_size(self, width, height): 

382 """ 

383 Set the size of a workbook window. 

384 

385 Args: 

386 width: Width of the window in pixels. 

387 height: Height of the window in pixels. 

388 

389 Returns: 

390 Nothing. 

391 

392 """ 

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

394 if width: 

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

396 else: 

397 self.window_width = 16095 

398 

399 if height: 

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

401 else: 

402 self.window_height = 9660 

403 

404 def set_tab_ratio(self, tab_ratio=None): 

405 """ 

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

407 

408 Args: 

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

410 

411 Returns: 

412 Nothing. 

413 

414 """ 

415 if tab_ratio is None: 

416 return 

417 

418 if tab_ratio < 0 or tab_ratio > 100: 

419 warn(f"Tab ratio '{tab_ratio}' outside: 0 <= tab_ratio <= 100") 

420 else: 

421 self.tab_ratio = int(tab_ratio * 10) 

422 

423 def set_properties(self, properties): 

424 """ 

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

426 

427 Args: 

428 properties: Dictionary of document properties. 

429 

430 Returns: 

431 Nothing. 

432 

433 """ 

434 self.doc_properties = properties 

435 

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

437 """ 

438 Set a custom document property. 

439 

440 Args: 

441 name: The name of the custom property. 

442 value: The value of the custom property. 

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

444 

445 Returns: 

446 0 on success. 

447 

448 """ 

449 if name is None or value is None: 

450 warn( 

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

452 "set_custom_property()" 

453 ) 

454 return -1 

455 

456 if property_type is None: 

457 # Determine the property type from the Python type. 

458 if isinstance(value, bool): 

459 property_type = "bool" 

460 elif isinstance(value, datetime): 

461 property_type = "date" 

462 elif isinstance(value, int): 

463 property_type = "number_int" 

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

465 property_type = "number" 

466 else: 

467 property_type = "text" 

468 

469 # Convert non-string values to strings to have a single data type. 

470 if property_type == "bool": 

471 value = str(value).lower() 

472 

473 if property_type == "date": 

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

475 

476 if property_type in ("number", "number_int"): 

477 value = str(value) 

478 

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

480 warn( 

481 f"Length of 'value' parameter exceeds Excel's limit of 255 " 

482 f"characters in set_custom_property(): '{value}'" 

483 ) 

484 

485 if len(name) > 255: 

486 warn( 

487 f"Length of 'name' parameter exceeds Excel's limit of 255 " 

488 f"characters in set_custom_property(): '{name}'" 

489 ) 

490 

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

492 

493 return 0 

494 

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

496 """ 

497 Set the Excel calculation mode for the workbook. 

498 

499 Args: 

500 mode: String containing one of: 

501 * manual 

502 * auto_except_tables 

503 * auto 

504 

505 Returns: 

506 Nothing. 

507 

508 """ 

509 self.calc_mode = mode 

510 

511 if mode == "manual": 

512 self.calc_on_load = False 

513 elif mode == "auto_except_tables": 

514 self.calc_mode = "autoNoTable" 

515 

516 # Leave undocumented for now. Rarely required. 

517 if calc_id: 

518 self.calc_id = calc_id 

519 

520 def define_name(self, name, formula): 

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

522 # names and local/worksheet names. 

523 """ 

524 Create a defined name in the workbook. 

525 

526 Args: 

527 name: The defined name. 

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

529 

530 Returns: 

531 0 on success. 

532 

533 """ 

534 sheet_index = None 

535 sheetname = "" 

536 

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

538 if formula.startswith("="): 

539 formula = formula.lstrip("=") 

540 

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

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

543 match = sheet_parts.match(name) 

544 

545 if match: 

546 sheetname = match.group(1) 

547 name = match.group(2) 

548 sheet_index = self._get_sheet_index(sheetname) 

549 

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

551 if sheet_index is None: 

552 warn(f"Unknown sheet name '{sheetname}' in defined_name()") 

553 return -1 

554 else: 

555 # Use -1 to indicate global names. 

556 sheet_index = -1 

557 

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

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

560 r"^\d", name 

561 ): 

562 warn(f"Invalid Excel characters in defined_name(): '{name}'") 

563 return -1 

564 

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

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

567 warn(f"Name looks like a cell name in defined_name(): '{name}'") 

568 return -1 

569 

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

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

572 warn(f"Invalid name '{name}' like a RC cell ref in defined_name()") 

573 return -1 

574 

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

576 

577 return 0 

578 

579 def worksheets(self): 

580 """ 

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

582 

583 Args: 

584 None. 

585 

586 Returns: 

587 A list of worksheet objects. 

588 

589 """ 

590 return self.worksheets_objs 

591 

592 def get_worksheet_by_name(self, name): 

593 """ 

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

595 

596 Args: 

597 name: The name of the worksheet. 

598 

599 Returns: 

600 A worksheet object or None. 

601 

602 """ 

603 return self.sheetnames.get(name) 

604 

605 def get_default_url_format(self): 

606 """ 

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

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

609 by Excel for the default theme. 

610 

611 Args: 

612 None. 

613 

614 Returns: 

615 A format object. 

616 

617 """ 

618 return self.default_url_format 

619 

620 def use_zip64(self): 

621 """ 

622 Allow ZIP64 extensions when writing xlsx file zip container. 

623 

624 Args: 

625 None. 

626 

627 Returns: 

628 Nothing. 

629 

630 """ 

631 self.allow_zip64 = True 

632 

633 def set_vba_name(self, name=None): 

634 """ 

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

636 to as ThisWorkbook in VBA. 

637 

638 Args: 

639 name: The VBA name for the workbook. 

640 

641 Returns: 

642 Nothing. 

643 

644 """ 

645 if name is not None: 

646 self.vba_codename = name 

647 else: 

648 self.vba_codename = "ThisWorkbook" 

649 

650 def read_only_recommended(self): 

651 """ 

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

653 

654 Args: 

655 None. 

656 

657 Returns: 

658 Nothing. 

659 

660 """ 

661 self.read_only = 2 

662 

663 ########################################################################### 

664 # 

665 # Private API. 

666 # 

667 ########################################################################### 

668 

669 def _assemble_xml_file(self): 

670 # Assemble and write the XML file. 

671 

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

673 self._prepare_format_properties() 

674 

675 # Write the XML declaration. 

676 self._xml_declaration() 

677 

678 # Write the workbook element. 

679 self._write_workbook() 

680 

681 # Write the fileVersion element. 

682 self._write_file_version() 

683 

684 # Write the fileSharing element. 

685 self._write_file_sharing() 

686 

687 # Write the workbookPr element. 

688 self._write_workbook_pr() 

689 

690 # Write the bookViews element. 

691 self._write_book_views() 

692 

693 # Write the sheets element. 

694 self._write_sheets() 

695 

696 # Write the workbook defined names. 

697 self._write_defined_names() 

698 

699 # Write the calcPr element. 

700 self._write_calc_pr() 

701 

702 # Close the workbook tag. 

703 self._xml_end_tag("workbook") 

704 

705 # Close the file. 

706 self._xml_close() 

707 

708 def _store_workbook(self): 

709 # pylint: disable=consider-using-with 

710 # Create the xlsx/zip file. 

711 try: 

712 xlsx_file = ZipFile( 

713 self.filename, 

714 "w", 

715 compression=ZIP_DEFLATED, 

716 allowZip64=self.allow_zip64, 

717 ) 

718 except IOError as e: 

719 raise e 

720 

721 # Assemble worksheets into a workbook. 

722 packager = self._get_packager() 

723 

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

725 if not self.worksheets(): 

726 self.add_worksheet() 

727 

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

729 if self.worksheet_meta.activesheet == 0: 

730 self.worksheets_objs[0].selected = 1 

731 self.worksheets_objs[0].hidden = 0 

732 

733 # Set the active sheet. 

734 for sheet in self.worksheets(): 

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

736 sheet.active = 1 

737 

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

739 if self.vba_project: 

740 for sheet in self.worksheets(): 

741 if sheet.vba_codename is None: 

742 sheet.set_vba_name() 

743 

744 # Convert the SST strings data structure. 

745 self._prepare_sst_string_data() 

746 

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

748 self._prepare_vml() 

749 

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

751 self._prepare_defined_names() 

752 

753 # Prepare the drawings, charts and images. 

754 self._prepare_drawings() 

755 

756 # Add cached data to charts. 

757 self._add_chart_data() 

758 

759 # Prepare the worksheet tables. 

760 self._prepare_tables() 

761 

762 # Prepare the metadata file links. 

763 self._prepare_metadata() 

764 

765 # Package the workbook. 

766 packager._add_workbook(self) 

767 packager._set_tmpdir(self.tmpdir) 

768 packager._set_in_memory(self.in_memory) 

769 xml_files = packager._create_package() 

770 

771 # Free up the Packager object. 

772 packager = None 

773 

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

775 for file_id, file_data in enumerate(xml_files): 

776 os_filename, xml_filename, is_binary = file_data 

777 

778 if self.in_memory: 

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

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

781 

782 # Copy compression type from parent ZipFile. 

783 zipinfo.compress_type = xlsx_file.compression 

784 

785 if is_binary: 

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

787 else: 

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

789 else: 

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

791 

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

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

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

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

796 

797 try: 

798 xlsx_file.write(os_filename, xml_filename) 

799 os.remove(os_filename) 

800 except LargeZipFile as e: 

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

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

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

804 raise e 

805 

806 xlsx_file.close() 

807 

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

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

810 

811 if worksheet_class: 

812 worksheet = worksheet_class() 

813 else: 

814 worksheet = self.worksheet_class() 

815 

816 sheet_index = len(self.worksheets_objs) 

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

818 

819 # Initialization data to pass to the worksheet. 

820 init_data = { 

821 "name": name, 

822 "index": sheet_index, 

823 "str_table": self.str_table, 

824 "worksheet_meta": self.worksheet_meta, 

825 "constant_memory": self.constant_memory, 

826 "tmpdir": self.tmpdir, 

827 "date_1904": self.date_1904, 

828 "strings_to_numbers": self.strings_to_numbers, 

829 "strings_to_formulas": self.strings_to_formulas, 

830 "strings_to_urls": self.strings_to_urls, 

831 "nan_inf_to_errors": self.nan_inf_to_errors, 

832 "default_date_format": self.default_date_format, 

833 "default_url_format": self.default_url_format, 

834 "workbook_add_format": self.add_format, 

835 "excel2003_style": self.excel2003_style, 

836 "remove_timezone": self.remove_timezone, 

837 "max_url_length": self.max_url_length, 

838 "use_future_functions": self.use_future_functions, 

839 "embedded_images": self.embedded_images, 

840 } 

841 

842 worksheet._initialize(init_data) 

843 

844 self.worksheets_objs.append(worksheet) 

845 self.sheetnames[name] = worksheet 

846 

847 return worksheet 

848 

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

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

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

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

853 

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

855 if is_chartsheet: 

856 self.chartname_count += 1 

857 else: 

858 self.sheetname_count += 1 

859 

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

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

862 if is_chartsheet: 

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

864 else: 

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

866 

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

868 if len(sheetname) > 31: 

869 raise InvalidWorksheetName( 

870 f"Excel worksheet name '{sheetname}' must be <= 31 chars." 

871 ) 

872 

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

874 if invalid_char.search(sheetname): 

875 raise InvalidWorksheetName( 

876 f"Invalid Excel character '[]:*?/\\' in sheetname '{sheetname}'." 

877 ) 

878 

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

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

881 raise InvalidWorksheetName( 

882 f'Sheet name cannot start or end with an apostrophe "{sheetname}".' 

883 ) 

884 

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

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

887 for worksheet in self.worksheets(): 

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

889 raise DuplicateWorksheetName( 

890 f"Sheetname '{sheetname}', with case ignored, is already in use." 

891 ) 

892 

893 return sheetname 

894 

895 def _prepare_format_properties(self): 

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

897 

898 # Separate format objects into XF and DXF formats. 

899 self._prepare_formats() 

900 

901 # Set the font index for the format objects. 

902 self._prepare_fonts() 

903 

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

905 self._prepare_num_formats() 

906 

907 # Set the border index for the format objects. 

908 self._prepare_borders() 

909 

910 # Set the fill index for the format objects. 

911 self._prepare_fills() 

912 

913 def _prepare_formats(self): 

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

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

916 # back into index order rather than creation order. 

917 xf_formats = [] 

918 dxf_formats = [] 

919 

920 # Sort into XF and DXF formats. 

921 for xf_format in self.formats: 

922 if xf_format.xf_index is not None: 

923 xf_formats.append(xf_format) 

924 

925 if xf_format.dxf_index is not None: 

926 dxf_formats.append(xf_format) 

927 

928 # Pre-extend the format lists. 

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

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

931 

932 # Rearrange formats into index order. 

933 for xf_format in xf_formats: 

934 index = xf_format.xf_index 

935 self.xf_formats[index] = xf_format 

936 

937 for dxf_format in dxf_formats: 

938 index = dxf_format.dxf_index 

939 self.dxf_formats[index] = dxf_format 

940 

941 def _set_default_xf_indices(self): 

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

943 

944 formats = list(self.formats) 

945 

946 # Delete the default url format. 

947 del formats[1] 

948 

949 # Skip the default date format if set. 

950 if self.default_date_format is not None: 

951 del formats[1] 

952 

953 # Set the remaining formats. 

954 for xf_format in formats: 

955 xf_format._get_xf_index() 

956 

957 def _prepare_fonts(self): 

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

959 # non-default font elements. 

960 fonts = {} 

961 index = 0 

962 

963 for xf_format in self.xf_formats: 

964 key = xf_format._get_font_key() 

965 if key in fonts: 

966 # Font has already been used. 

967 xf_format.font_index = fonts[key] 

968 xf_format.has_font = False 

969 else: 

970 # This is a new font. 

971 fonts[key] = index 

972 xf_format.font_index = index 

973 xf_format.has_font = True 

974 index += 1 

975 

976 self.font_count = index 

977 

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

979 for xf_format in self.dxf_formats: 

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

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

982 if ( 

983 xf_format.font_color 

984 or xf_format.bold 

985 or xf_format.italic 

986 or xf_format.underline 

987 or xf_format.font_strikeout 

988 ): 

989 xf_format.has_dxf_font = True 

990 

991 def _prepare_num_formats(self): 

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

993 unique_num_formats = {} 

994 num_formats = [] 

995 index = 164 

996 

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

998 num_format = xf_format.num_format 

999 

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

1001 if not isinstance(num_format, str): 

1002 num_format = int(num_format) 

1003 

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

1005 if num_format == 0: 

1006 num_format = 1 

1007 

1008 xf_format.num_format_index = num_format 

1009 continue 

1010 

1011 if num_format == "0": 

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

1013 xf_format.num_format_index = 1 

1014 continue 

1015 

1016 if num_format == "General": 

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

1018 xf_format.num_format_index = 0 

1019 continue 

1020 

1021 if num_format in unique_num_formats: 

1022 # Number xf_format has already been used. 

1023 xf_format.num_format_index = unique_num_formats[num_format] 

1024 else: 

1025 # Add a new number xf_format. 

1026 unique_num_formats[num_format] = index 

1027 xf_format.num_format_index = index 

1028 index += 1 

1029 

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

1031 if xf_format.xf_index: 

1032 num_formats.append(num_format) 

1033 

1034 self.num_formats = num_formats 

1035 

1036 def _prepare_borders(self): 

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

1038 # non-default border elements. 

1039 borders = {} 

1040 index = 0 

1041 

1042 for xf_format in self.xf_formats: 

1043 key = xf_format._get_border_key() 

1044 

1045 if key in borders: 

1046 # Border has already been used. 

1047 xf_format.border_index = borders[key] 

1048 xf_format.has_border = False 

1049 else: 

1050 # This is a new border. 

1051 borders[key] = index 

1052 xf_format.border_index = index 

1053 xf_format.has_border = True 

1054 index += 1 

1055 

1056 self.border_count = index 

1057 

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

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

1060 

1061 for xf_format in self.dxf_formats: 

1062 key = xf_format._get_border_key() 

1063 

1064 if has_border.search(key): 

1065 xf_format.has_dxf_border = True 

1066 

1067 def _prepare_fills(self): 

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

1069 # non-default fill elements. 

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

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

1072 fills = {} 

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

1074 

1075 # Add the default fills. 

1076 fills["0:None:None"] = 0 

1077 fills["17:None:None"] = 1 

1078 

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

1080 for xf_format in self.dxf_formats: 

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

1082 xf_format.has_dxf_fill = True 

1083 xf_format.dxf_bg_color = xf_format.bg_color 

1084 xf_format.dxf_fg_color = xf_format.fg_color 

1085 

1086 for xf_format in self.xf_formats: 

1087 # The following logical statements jointly take care of special 

1088 # cases in relation to cell colors and patterns: 

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

1090 # foreground and background colors, and 

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

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

1093 # in the defaults. 

1094 if xf_format.pattern == 1 and xf_format.bg_color and xf_format.fg_color: 

1095 tmp = xf_format.fg_color 

1096 xf_format.fg_color = xf_format.bg_color 

1097 xf_format.bg_color = tmp 

1098 

1099 if xf_format.pattern <= 1 and xf_format.bg_color and not xf_format.fg_color: 

1100 xf_format.fg_color = xf_format.bg_color 

1101 xf_format.bg_color = None 

1102 xf_format.pattern = 1 

1103 

1104 if xf_format.pattern <= 1 and not xf_format.bg_color and xf_format.fg_color: 

1105 xf_format.pattern = 1 

1106 

1107 key = xf_format._get_fill_key() 

1108 

1109 if key in fills: 

1110 # Fill has already been used. 

1111 xf_format.fill_index = fills[key] 

1112 xf_format.has_fill = False 

1113 else: 

1114 # This is a new fill. 

1115 fills[key] = index 

1116 xf_format.fill_index = index 

1117 xf_format.has_fill = True 

1118 index += 1 

1119 

1120 self.fill_count = index 

1121 

1122 def _has_feature_property_bags(self): 

1123 # Check for any format properties that require a feature bag. Currently 

1124 # this only applies to checkboxes. 

1125 if not self.feature_property_bags: 

1126 for xf_format in self.formats: 

1127 if xf_format.checkbox: 

1128 self.feature_property_bags.add("XFComplements") 

1129 

1130 if xf_format.dxf_index is not None and xf_format.checkbox: 

1131 self.feature_property_bags.add("DXFComplements") 

1132 

1133 return self.feature_property_bags 

1134 

1135 def _prepare_defined_names(self): 

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

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

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

1139 defined_names = self.defined_names 

1140 

1141 for sheet in self.worksheets(): 

1142 # Check for Print Area settings. 

1143 if sheet.autofilter_area: 

1144 hidden = 1 

1145 sheet_range = sheet.autofilter_area 

1146 # Store the defined names. 

1147 defined_names.append( 

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

1149 ) 

1150 

1151 # Check for Print Area settings. 

1152 if sheet.print_area_range: 

1153 hidden = 0 

1154 sheet_range = sheet.print_area_range 

1155 # Store the defined names. 

1156 defined_names.append( 

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

1158 ) 

1159 

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

1161 if sheet.repeat_col_range or sheet.repeat_row_range: 

1162 hidden = 0 

1163 sheet_range = "" 

1164 if sheet.repeat_col_range and sheet.repeat_row_range: 

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

1166 else: 

1167 sheet_range = sheet.repeat_col_range + sheet.repeat_row_range 

1168 # Store the defined names. 

1169 defined_names.append( 

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

1171 ) 

1172 

1173 defined_names = self._sort_defined_names(defined_names) 

1174 self.defined_names = defined_names 

1175 self.named_ranges = self._extract_named_ranges(defined_names) 

1176 

1177 def _sort_defined_names(self, names): 

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

1179 # the same order as used by Excel. 

1180 

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

1182 for name_list in names: 

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

1184 

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

1186 # from internal names and lowercasing the string. 

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

1188 

1189 # Normalize the sheetname by removing the leading quote and 

1190 # lowercasing the string. 

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

1192 

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

1194 

1195 # Sort based on the normalized key. 

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

1197 

1198 # Remove the extra key used for sorting. 

1199 for name_list in names: 

1200 name_list.pop() 

1201 

1202 return names 

1203 

1204 def _prepare_drawings(self): 

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

1206 chart_ref_id = 0 

1207 ref_id = 0 

1208 drawing_id = 0 

1209 image_ids = {} 

1210 header_image_ids = {} 

1211 background_ids = {} 

1212 

1213 # Store the image types for any embedded images. 

1214 for image in self.embedded_images.images: 

1215 image_extension = image._image_extension 

1216 self.image_types[image_extension] = True 

1217 

1218 if image.description is not None: 

1219 self.has_embedded_descriptions = True 

1220 

1221 image_ref_id = len(self.embedded_images.images) 

1222 

1223 for sheet in self.worksheets(): 

1224 chart_count = len(sheet.charts) 

1225 image_count = len(sheet.images) 

1226 shape_count = len(sheet.shapes) 

1227 

1228 header_image_count = len(sheet.header_images) 

1229 footer_image_count = len(sheet.footer_images) 

1230 has_background = sheet.background_image 

1231 has_drawing = False 

1232 

1233 if not ( 

1234 chart_count 

1235 or image_count 

1236 or shape_count 

1237 or header_image_count 

1238 or footer_image_count 

1239 or has_background 

1240 ): 

1241 continue 

1242 

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

1244 if chart_count or image_count or shape_count: 

1245 drawing_id += 1 

1246 has_drawing = True 

1247 

1248 # Prepare the background images. 

1249 if sheet.background_image: 

1250 image = sheet.background_image 

1251 image_extension = image._image_extension 

1252 image_digest = image._digest 

1253 

1254 self.image_types[image_extension] = True 

1255 

1256 if image_digest in background_ids: 

1257 ref_id = background_ids[image_digest] 

1258 else: 

1259 image_ref_id += 1 

1260 ref_id = image_ref_id 

1261 background_ids[image_digest] = image_ref_id 

1262 self.images.append(image) 

1263 

1264 sheet._prepare_background(ref_id, image_extension) 

1265 

1266 # Prepare the worksheet images. 

1267 for index in range(image_count): 

1268 image = sheet.images[index] 

1269 image_extension = image._image_extension 

1270 image_digest = image._digest 

1271 

1272 self.image_types[image_extension] = True 

1273 

1274 if image_digest in image_ids: 

1275 ref_id = image_ids[image_digest] 

1276 else: 

1277 image_ref_id += 1 

1278 ref_id = image_ref_id 

1279 image_ids[image_digest] = image_ref_id 

1280 self.images.append(image) 

1281 

1282 sheet._prepare_image( 

1283 image, 

1284 ref_id, 

1285 drawing_id, 

1286 ) 

1287 

1288 # Prepare the worksheet charts. 

1289 for index in range(chart_count): 

1290 chart_ref_id += 1 

1291 sheet._prepare_chart(index, chart_ref_id, drawing_id) 

1292 

1293 # Prepare the worksheet shapes. 

1294 for index in range(shape_count): 

1295 sheet._prepare_shape(index, drawing_id) 

1296 

1297 # Prepare the header images. 

1298 for image in sheet.header_images: 

1299 image_extension = image._image_extension 

1300 image_digest = image._digest 

1301 

1302 self.image_types[image_extension] = True 

1303 

1304 if image_digest in header_image_ids: 

1305 ref_id = header_image_ids[image_digest] 

1306 else: 

1307 image_ref_id += 1 

1308 ref_id = image_ref_id 

1309 header_image_ids[image_digest] = image_ref_id 

1310 self.images.append(image) 

1311 

1312 sheet._prepare_header_image(ref_id, image) 

1313 

1314 # Prepare the footer images. 

1315 for image in sheet.footer_images: 

1316 image_extension = image._image_extension 

1317 image_digest = image._digest 

1318 

1319 self.image_types[image_extension] = True 

1320 

1321 if image_digest in header_image_ids: 

1322 ref_id = header_image_ids[image_digest] 

1323 else: 

1324 image_ref_id += 1 

1325 ref_id = image_ref_id 

1326 header_image_ids[image_digest] = image_ref_id 

1327 self.images.append(image) 

1328 

1329 sheet._prepare_header_image(ref_id, image) 

1330 

1331 if has_drawing: 

1332 drawing = sheet.drawing 

1333 self.drawings.append(drawing) 

1334 

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

1336 for chart in self.charts[:]: 

1337 if chart.id == -1: 

1338 self.charts.remove(chart) 

1339 

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

1341 # written to the worksheets above. 

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

1343 

1344 self.drawing_count = drawing_id 

1345 

1346 def _extract_named_ranges(self, defined_names): 

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

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

1349 named_ranges = [] 

1350 

1351 for defined_name in defined_names: 

1352 name = defined_name[0] 

1353 index = defined_name[1] 

1354 sheet_range = defined_name[2] 

1355 

1356 # Skip autoFilter ranges. 

1357 if name == "_xlnm._FilterDatabase": 

1358 continue 

1359 

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

1361 if "!" in sheet_range: 

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

1363 

1364 # Match Print_Area and Print_Titles xlnm types. 

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

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

1367 name = sheet_name + "!" + xlnm_type 

1368 elif index != -1: 

1369 name = sheet_name + "!" + name 

1370 

1371 named_ranges.append(name) 

1372 

1373 return named_ranges 

1374 

1375 def _get_sheet_index(self, sheetname): 

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

1377 sheetname = sheetname.strip("'") 

1378 

1379 if sheetname in self.sheetnames: 

1380 return self.sheetnames[sheetname].index 

1381 

1382 return None 

1383 

1384 def _prepare_vml(self): 

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

1386 comment_id = 0 

1387 vml_drawing_id = 0 

1388 vml_data_id = 1 

1389 vml_header_id = 0 

1390 vml_shape_id = 1024 

1391 vml_files = 0 

1392 comment_files = 0 

1393 

1394 for sheet in self.worksheets(): 

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

1396 continue 

1397 

1398 vml_files += 1 

1399 

1400 if sheet.has_vml: 

1401 if sheet.has_comments: 

1402 comment_files += 1 

1403 comment_id += 1 

1404 self.has_comments = True 

1405 

1406 vml_drawing_id += 1 

1407 

1408 count = sheet._prepare_vml_objects( 

1409 vml_data_id, vml_shape_id, vml_drawing_id, comment_id 

1410 ) 

1411 

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

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

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

1415 

1416 if sheet.has_header_vml: 

1417 vml_header_id += 1 

1418 vml_drawing_id += 1 

1419 sheet._prepare_header_vml_objects(vml_header_id, vml_drawing_id) 

1420 

1421 self.num_vml_files = vml_files 

1422 self.num_comment_files = comment_files 

1423 

1424 def _prepare_tables(self): 

1425 # Set the table ids for the worksheet tables. 

1426 table_id = 0 

1427 seen = {} 

1428 

1429 for sheet in self.worksheets(): 

1430 table_count = len(sheet.tables) 

1431 

1432 if not table_count: 

1433 continue 

1434 

1435 sheet._prepare_tables(table_id + 1, seen) 

1436 table_id += table_count 

1437 

1438 def _prepare_metadata(self): 

1439 # Set the metadata rel link. 

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

1441 self.has_metadata = self.has_embedded_images 

1442 

1443 for sheet in self.worksheets(): 

1444 if sheet.has_dynamic_arrays: 

1445 self.has_metadata = True 

1446 self.has_dynamic_functions = True 

1447 

1448 def _add_chart_data(self): 

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

1450 # data for series and title/axis ranges. 

1451 worksheets = {} 

1452 seen_ranges = {} 

1453 charts = [] 

1454 

1455 # Map worksheet names to worksheet objects. 

1456 for worksheet in self.worksheets(): 

1457 worksheets[worksheet.name] = worksheet 

1458 

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

1460 for chart in self.charts: 

1461 charts.append(chart) 

1462 if chart.combined: 

1463 charts.append(chart.combined) 

1464 

1465 for chart in charts: 

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

1467 r_id = chart.formula_ids[c_range] 

1468 

1469 # Skip if the series has user defined data. 

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

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

1472 data = chart.formula_data[r_id] 

1473 seen_ranges[c_range] = data 

1474 continue 

1475 

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

1477 if c_range in seen_ranges: 

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

1479 continue 

1480 

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

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

1483 

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

1485 if sheetname is None: 

1486 continue 

1487 

1488 # Handle non-contiguous ranges like: 

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

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

1491 if sheetname.startswith("("): 

1492 chart.formula_data[r_id] = [] 

1493 seen_ranges[c_range] = [] 

1494 continue 

1495 

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

1497 # in a chart series formula. 

1498 if sheetname not in worksheets: 

1499 warn( 

1500 f"Unknown worksheet reference '{sheetname}' in range " 

1501 f"'{c_range}' passed to add_series()" 

1502 ) 

1503 chart.formula_data[r_id] = [] 

1504 seen_ranges[c_range] = [] 

1505 continue 

1506 

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

1508 worksheet = worksheets[sheetname] 

1509 

1510 # Get the data from the worksheet table. 

1511 data = worksheet._get_range_data(*cells) 

1512 

1513 # Add the data to the chart. 

1514 chart.formula_data[r_id] = data 

1515 

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

1517 seen_ranges[c_range] = data 

1518 

1519 def _get_chart_range(self, c_range): 

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

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

1522 

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

1524 pos = c_range.rfind("!") 

1525 if pos > 0: 

1526 sheetname = c_range[:pos] 

1527 cells = c_range[pos + 1 :] 

1528 else: 

1529 return None, None 

1530 

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

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

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

1534 else: 

1535 (cell_1, cell_2) = (cells, cells) 

1536 

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

1538 sheetname = sheetname.strip("'") 

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

1540 

1541 try: 

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

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

1544 (row_start, col_start) = xl_cell_to_rowcol(cell_1) 

1545 (row_end, col_end) = xl_cell_to_rowcol(cell_2) 

1546 except AttributeError: 

1547 return None, None 

1548 

1549 # We only handle 1D ranges. 

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

1551 return None, None 

1552 

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

1554 

1555 def _prepare_sst_string_data(self): 

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

1557 self.str_table._sort_string_data() 

1558 

1559 def _get_packager(self): 

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

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

1562 return Packager() 

1563 

1564 ########################################################################### 

1565 # 

1566 # XML methods. 

1567 # 

1568 ########################################################################### 

1569 

1570 def _write_workbook(self): 

1571 # Write <workbook> element. 

1572 

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

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

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

1576 

1577 attributes = [ 

1578 ("xmlns", xmlns), 

1579 ("xmlns:r", xmlns_r), 

1580 ] 

1581 

1582 self._xml_start_tag("workbook", attributes) 

1583 

1584 def _write_file_version(self): 

1585 # Write the <fileVersion> element. 

1586 

1587 app_name = "xl" 

1588 last_edited = 4 

1589 lowest_edited = 4 

1590 rup_build = 4505 

1591 

1592 attributes = [ 

1593 ("appName", app_name), 

1594 ("lastEdited", last_edited), 

1595 ("lowestEdited", lowest_edited), 

1596 ("rupBuild", rup_build), 

1597 ] 

1598 

1599 if self.vba_project: 

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

1601 

1602 self._xml_empty_tag("fileVersion", attributes) 

1603 

1604 def _write_file_sharing(self): 

1605 # Write the <fileSharing> element. 

1606 if self.read_only == 0: 

1607 return 

1608 

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

1610 

1611 self._xml_empty_tag("fileSharing", attributes) 

1612 

1613 def _write_workbook_pr(self): 

1614 # Write <workbookPr> element. 

1615 default_theme_version = 124226 

1616 attributes = [] 

1617 

1618 if self.vba_codename: 

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

1620 if self.date_1904: 

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

1622 

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

1624 

1625 self._xml_empty_tag("workbookPr", attributes) 

1626 

1627 def _write_book_views(self): 

1628 # Write <bookViews> element. 

1629 self._xml_start_tag("bookViews") 

1630 self._write_workbook_view() 

1631 self._xml_end_tag("bookViews") 

1632 

1633 def _write_workbook_view(self): 

1634 # Write <workbookView> element. 

1635 attributes = [ 

1636 ("xWindow", self.x_window), 

1637 ("yWindow", self.y_window), 

1638 ("windowWidth", self.window_width), 

1639 ("windowHeight", self.window_height), 

1640 ] 

1641 

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

1643 if self.tab_ratio != 600: 

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

1645 

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

1647 if self.worksheet_meta.firstsheet > 0: 

1648 firstsheet = self.worksheet_meta.firstsheet + 1 

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

1650 

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

1652 if self.worksheet_meta.activesheet > 0: 

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

1654 

1655 self._xml_empty_tag("workbookView", attributes) 

1656 

1657 def _write_sheets(self): 

1658 # Write <sheets> element. 

1659 self._xml_start_tag("sheets") 

1660 

1661 id_num = 1 

1662 for worksheet in self.worksheets(): 

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

1664 id_num += 1 

1665 

1666 self._xml_end_tag("sheets") 

1667 

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

1669 # Write <sheet> element. 

1670 attributes = [ 

1671 ("name", name), 

1672 ("sheetId", sheet_id), 

1673 ] 

1674 

1675 if hidden == 1: 

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

1677 elif hidden == 2: 

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

1679 

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

1681 

1682 self._xml_empty_tag("sheet", attributes) 

1683 

1684 def _write_calc_pr(self): 

1685 # Write the <calcPr> element. 

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

1687 

1688 if self.calc_mode == "manual": 

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

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

1691 elif self.calc_mode == "autoNoTable": 

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

1693 

1694 if self.calc_on_load: 

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

1696 

1697 self._xml_empty_tag("calcPr", attributes) 

1698 

1699 def _write_defined_names(self): 

1700 # Write the <definedNames> element. 

1701 if not self.defined_names: 

1702 return 

1703 

1704 self._xml_start_tag("definedNames") 

1705 

1706 for defined_name in self.defined_names: 

1707 self._write_defined_name(defined_name) 

1708 

1709 self._xml_end_tag("definedNames") 

1710 

1711 def _write_defined_name(self, defined_name): 

1712 # Write the <definedName> element. 

1713 name = defined_name[0] 

1714 sheet_id = defined_name[1] 

1715 sheet_range = defined_name[2] 

1716 hidden = defined_name[3] 

1717 

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

1719 

1720 if sheet_id != -1: 

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

1722 if hidden: 

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

1724 

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

1726 

1727 

1728# A metadata class to share data between worksheets. 

1729class WorksheetMeta: 

1730 """ 

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

1732 first sheet. 

1733 

1734 """ 

1735 

1736 def __init__(self): 

1737 self.activesheet = 0 

1738 self.firstsheet = 0 

1739 

1740 

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

1742class EmbeddedImages: 

1743 """ 

1744 A class to track duplicate embedded images between worksheets. 

1745 

1746 """ 

1747 

1748 def __init__(self): 

1749 self.images = [] 

1750 self.image_indexes = {} 

1751 

1752 def get_image_index(self, image: Image): 

1753 """ 

1754 Get the index of an embedded image. 

1755 

1756 Args: 

1757 image: The image to lookup. 

1758 

1759 Returns: 

1760 The image index. 

1761 

1762 """ 

1763 image_index = self.image_indexes.get(image._digest) 

1764 

1765 if image_index is None: 

1766 self.images.append(image) 

1767 image_index = len(self.images) 

1768 self.image_indexes[image._digest] = image_index 

1769 

1770 return image_index 

1771 

1772 def has_images(self): 

1773 """ 

1774 Check if the worksheet has embedded images. 

1775 

1776 Args: 

1777 None. 

1778 

1779 Returns: 

1780 Boolean. 

1781 

1782 """ 

1783 return len(self.images) > 0