123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- from __future__ import annotations
- from typing import Any
- from pandas._libs import json
- from pandas._typing import (
- FilePath,
- StorageOptions,
- WriteExcelBuffer,
- )
- from pandas.io.excel._base import ExcelWriter
- from pandas.io.excel._util import (
- combine_kwargs,
- validate_freeze_panes,
- )
- class _XlsxStyler:
- # Map from openpyxl-oriented styles to flatter xlsxwriter representation
- # Ordering necessary for both determinism and because some are keyed by
- # prefixes of others.
- STYLE_MAPPING: dict[str, list[tuple[tuple[str, ...], str]]] = {
- "font": [
- (("name",), "font_name"),
- (("sz",), "font_size"),
- (("size",), "font_size"),
- (("color", "rgb"), "font_color"),
- (("color",), "font_color"),
- (("b",), "bold"),
- (("bold",), "bold"),
- (("i",), "italic"),
- (("italic",), "italic"),
- (("u",), "underline"),
- (("underline",), "underline"),
- (("strike",), "font_strikeout"),
- (("vertAlign",), "font_script"),
- (("vertalign",), "font_script"),
- ],
- "number_format": [(("format_code",), "num_format"), ((), "num_format")],
- "protection": [(("locked",), "locked"), (("hidden",), "hidden")],
- "alignment": [
- (("horizontal",), "align"),
- (("vertical",), "valign"),
- (("text_rotation",), "rotation"),
- (("wrap_text",), "text_wrap"),
- (("indent",), "indent"),
- (("shrink_to_fit",), "shrink"),
- ],
- "fill": [
- (("patternType",), "pattern"),
- (("patterntype",), "pattern"),
- (("fill_type",), "pattern"),
- (("start_color", "rgb"), "fg_color"),
- (("fgColor", "rgb"), "fg_color"),
- (("fgcolor", "rgb"), "fg_color"),
- (("start_color",), "fg_color"),
- (("fgColor",), "fg_color"),
- (("fgcolor",), "fg_color"),
- (("end_color", "rgb"), "bg_color"),
- (("bgColor", "rgb"), "bg_color"),
- (("bgcolor", "rgb"), "bg_color"),
- (("end_color",), "bg_color"),
- (("bgColor",), "bg_color"),
- (("bgcolor",), "bg_color"),
- ],
- "border": [
- (("color", "rgb"), "border_color"),
- (("color",), "border_color"),
- (("style",), "border"),
- (("top", "color", "rgb"), "top_color"),
- (("top", "color"), "top_color"),
- (("top", "style"), "top"),
- (("top",), "top"),
- (("right", "color", "rgb"), "right_color"),
- (("right", "color"), "right_color"),
- (("right", "style"), "right"),
- (("right",), "right"),
- (("bottom", "color", "rgb"), "bottom_color"),
- (("bottom", "color"), "bottom_color"),
- (("bottom", "style"), "bottom"),
- (("bottom",), "bottom"),
- (("left", "color", "rgb"), "left_color"),
- (("left", "color"), "left_color"),
- (("left", "style"), "left"),
- (("left",), "left"),
- ],
- }
- @classmethod
- def convert(cls, style_dict, num_format_str=None):
- """
- converts a style_dict to an xlsxwriter format dict
- Parameters
- ----------
- style_dict : style dictionary to convert
- num_format_str : optional number format string
- """
- # Create a XlsxWriter format object.
- props = {}
- if num_format_str is not None:
- props["num_format"] = num_format_str
- if style_dict is None:
- return props
- if "borders" in style_dict:
- style_dict = style_dict.copy()
- style_dict["border"] = style_dict.pop("borders")
- for style_group_key, style_group in style_dict.items():
- for src, dst in cls.STYLE_MAPPING.get(style_group_key, []):
- # src is a sequence of keys into a nested dict
- # dst is a flat key
- if dst in props:
- continue
- v = style_group
- for k in src:
- try:
- v = v[k]
- except (KeyError, TypeError):
- break
- else:
- props[dst] = v
- if isinstance(props.get("pattern"), str):
- # TODO: support other fill patterns
- props["pattern"] = 0 if props["pattern"] == "none" else 1
- for k in ["border", "top", "right", "bottom", "left"]:
- if isinstance(props.get(k), str):
- try:
- props[k] = [
- "none",
- "thin",
- "medium",
- "dashed",
- "dotted",
- "thick",
- "double",
- "hair",
- "mediumDashed",
- "dashDot",
- "mediumDashDot",
- "dashDotDot",
- "mediumDashDotDot",
- "slantDashDot",
- ].index(props[k])
- except ValueError:
- props[k] = 2
- if isinstance(props.get("font_script"), str):
- props["font_script"] = ["baseline", "superscript", "subscript"].index(
- props["font_script"]
- )
- if isinstance(props.get("underline"), str):
- props["underline"] = {
- "none": 0,
- "single": 1,
- "double": 2,
- "singleAccounting": 33,
- "doubleAccounting": 34,
- }[props["underline"]]
- # GH 30107 - xlsxwriter uses different name
- if props.get("valign") == "center":
- props["valign"] = "vcenter"
- return props
- class XlsxWriter(ExcelWriter):
- _engine = "xlsxwriter"
- _supported_extensions = (".xlsx",)
- def __init__(
- self,
- path: FilePath | WriteExcelBuffer | ExcelWriter,
- engine: str | None = None,
- date_format: str | None = None,
- datetime_format: str | None = None,
- mode: str = "w",
- storage_options: StorageOptions = None,
- if_sheet_exists: str | None = None,
- engine_kwargs: dict[str, Any] | None = None,
- **kwargs,
- ) -> None:
- # Use the xlsxwriter module as the Excel writer.
- from xlsxwriter import Workbook
- engine_kwargs = combine_kwargs(engine_kwargs, kwargs)
- if mode == "a":
- raise ValueError("Append mode is not supported with xlsxwriter!")
- super().__init__(
- path,
- engine=engine,
- date_format=date_format,
- datetime_format=datetime_format,
- mode=mode,
- storage_options=storage_options,
- if_sheet_exists=if_sheet_exists,
- engine_kwargs=engine_kwargs,
- )
- self._book = Workbook(self._handles.handle, **engine_kwargs)
- @property
- def book(self):
- """
- Book instance of class xlsxwriter.Workbook.
- This attribute can be used to access engine-specific features.
- """
- return self._book
- @property
- def sheets(self) -> dict[str, Any]:
- result = self.book.sheetnames
- return result
- def _save(self) -> None:
- """
- Save workbook to disk.
- """
- self.book.close()
- def _write_cells(
- self,
- cells,
- sheet_name: str | None = None,
- startrow: int = 0,
- startcol: int = 0,
- freeze_panes: tuple[int, int] | None = None,
- ) -> None:
- # Write the frame cells using xlsxwriter.
- sheet_name = self._get_sheet_name(sheet_name)
- wks = self.book.get_worksheet_by_name(sheet_name)
- if wks is None:
- wks = self.book.add_worksheet(sheet_name)
- style_dict = {"null": None}
- if validate_freeze_panes(freeze_panes):
- wks.freeze_panes(*(freeze_panes))
- for cell in cells:
- val, fmt = self._value_with_fmt(cell.val)
- stylekey = json.dumps(cell.style)
- if fmt:
- stylekey += fmt
- if stylekey in style_dict:
- style = style_dict[stylekey]
- else:
- style = self.book.add_format(_XlsxStyler.convert(cell.style, fmt))
- style_dict[stylekey] = style
- if cell.mergestart is not None and cell.mergeend is not None:
- wks.merge_range(
- startrow + cell.row,
- startcol + cell.col,
- startrow + cell.mergestart,
- startcol + cell.mergeend,
- val,
- style,
- )
- else:
- wks.write(startrow + cell.row, startcol + cell.col, val, style)
|