forked from Alsan/Post_finder
275 lines
8.6 KiB
Python
275 lines
8.6 KiB
Python
|
# Copyright (c) 2010-2024 openpyxl
|
||
|
|
||
|
from warnings import warn
|
||
|
|
||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||
|
from openpyxl.descriptors import (
|
||
|
Typed,
|
||
|
)
|
||
|
from openpyxl.descriptors.sequence import NestedSequence
|
||
|
from openpyxl.descriptors.excel import ExtensionList
|
||
|
from openpyxl.utils.indexed_list import IndexedList
|
||
|
from openpyxl.xml.constants import ARC_STYLE, SHEET_MAIN_NS
|
||
|
from openpyxl.xml.functions import fromstring
|
||
|
|
||
|
from .builtins import styles
|
||
|
from .colors import ColorList
|
||
|
from .differential import DifferentialStyle
|
||
|
from .table import TableStyleList
|
||
|
from .borders import Border
|
||
|
from .fills import Fill
|
||
|
from .fonts import Font
|
||
|
from .numbers import (
|
||
|
NumberFormatList,
|
||
|
BUILTIN_FORMATS,
|
||
|
BUILTIN_FORMATS_MAX_SIZE,
|
||
|
BUILTIN_FORMATS_REVERSE,
|
||
|
is_date_format,
|
||
|
is_timedelta_format,
|
||
|
builtin_format_code
|
||
|
)
|
||
|
from .named_styles import (
|
||
|
_NamedCellStyleList,
|
||
|
NamedStyleList,
|
||
|
NamedStyle,
|
||
|
)
|
||
|
from .cell_style import CellStyle, CellStyleList
|
||
|
|
||
|
|
||
|
class Stylesheet(Serialisable):
|
||
|
|
||
|
tagname = "styleSheet"
|
||
|
|
||
|
numFmts = Typed(expected_type=NumberFormatList)
|
||
|
fonts = NestedSequence(expected_type=Font, count=True)
|
||
|
fills = NestedSequence(expected_type=Fill, count=True)
|
||
|
borders = NestedSequence(expected_type=Border, count=True)
|
||
|
cellStyleXfs = Typed(expected_type=CellStyleList)
|
||
|
cellXfs = Typed(expected_type=CellStyleList)
|
||
|
cellStyles = Typed(expected_type=_NamedCellStyleList)
|
||
|
dxfs = NestedSequence(expected_type=DifferentialStyle, count=True)
|
||
|
tableStyles = Typed(expected_type=TableStyleList, allow_none=True)
|
||
|
colors = Typed(expected_type=ColorList, allow_none=True)
|
||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||
|
|
||
|
__elements__ = ('numFmts', 'fonts', 'fills', 'borders', 'cellStyleXfs',
|
||
|
'cellXfs', 'cellStyles', 'dxfs', 'tableStyles', 'colors')
|
||
|
|
||
|
def __init__(self,
|
||
|
numFmts=None,
|
||
|
fonts=(),
|
||
|
fills=(),
|
||
|
borders=(),
|
||
|
cellStyleXfs=None,
|
||
|
cellXfs=None,
|
||
|
cellStyles=None,
|
||
|
dxfs=(),
|
||
|
tableStyles=None,
|
||
|
colors=None,
|
||
|
extLst=None,
|
||
|
):
|
||
|
if numFmts is None:
|
||
|
numFmts = NumberFormatList()
|
||
|
self.numFmts = numFmts
|
||
|
self.number_formats = IndexedList()
|
||
|
self.fonts = fonts
|
||
|
self.fills = fills
|
||
|
self.borders = borders
|
||
|
if cellStyleXfs is None:
|
||
|
cellStyleXfs = CellStyleList()
|
||
|
self.cellStyleXfs = cellStyleXfs
|
||
|
if cellXfs is None:
|
||
|
cellXfs = CellStyleList()
|
||
|
self.cellXfs = cellXfs
|
||
|
if cellStyles is None:
|
||
|
cellStyles = _NamedCellStyleList()
|
||
|
self.cellStyles = cellStyles
|
||
|
|
||
|
self.dxfs = dxfs
|
||
|
self.tableStyles = tableStyles
|
||
|
self.colors = colors
|
||
|
|
||
|
self.cell_styles = self.cellXfs._to_array()
|
||
|
self.alignments = self.cellXfs.alignments
|
||
|
self.protections = self.cellXfs.prots
|
||
|
self._normalise_numbers()
|
||
|
self.named_styles = self._merge_named_styles()
|
||
|
|
||
|
|
||
|
@classmethod
|
||
|
def from_tree(cls, node):
|
||
|
# strip all attribs
|
||
|
attrs = dict(node.attrib)
|
||
|
for k in attrs:
|
||
|
del node.attrib[k]
|
||
|
return super().from_tree(node)
|
||
|
|
||
|
|
||
|
def _merge_named_styles(self):
|
||
|
"""
|
||
|
Merge named style names "cellStyles" with their associated styles
|
||
|
"cellStyleXfs"
|
||
|
"""
|
||
|
style_refs = self.cellStyles.remove_duplicates()
|
||
|
from_ref = [self._expand_named_style(style_ref) for style_ref in style_refs]
|
||
|
|
||
|
return NamedStyleList(from_ref)
|
||
|
|
||
|
|
||
|
def _expand_named_style(self, style_ref):
|
||
|
"""
|
||
|
Expand a named style reference element to a
|
||
|
named style object by binding the relevant
|
||
|
objects from the stylesheet
|
||
|
"""
|
||
|
xf = self.cellStyleXfs[style_ref.xfId]
|
||
|
named_style = NamedStyle(
|
||
|
name=style_ref.name,
|
||
|
hidden=style_ref.hidden,
|
||
|
builtinId=style_ref.builtinId,
|
||
|
)
|
||
|
|
||
|
named_style.font = self.fonts[xf.fontId]
|
||
|
named_style.fill = self.fills[xf.fillId]
|
||
|
named_style.border = self.borders[xf.borderId]
|
||
|
if xf.numFmtId < BUILTIN_FORMATS_MAX_SIZE:
|
||
|
formats = BUILTIN_FORMATS
|
||
|
else:
|
||
|
formats = self.custom_formats
|
||
|
|
||
|
if xf.numFmtId in formats:
|
||
|
named_style.number_format = formats[xf.numFmtId]
|
||
|
if xf.alignment:
|
||
|
named_style.alignment = xf.alignment
|
||
|
if xf.protection:
|
||
|
named_style.protection = xf.protection
|
||
|
|
||
|
return named_style
|
||
|
|
||
|
|
||
|
def _split_named_styles(self, wb):
|
||
|
"""
|
||
|
Convert NamedStyle into separate CellStyle and Xf objects
|
||
|
|
||
|
"""
|
||
|
for style in wb._named_styles:
|
||
|
self.cellStyles.cellStyle.append(style.as_name())
|
||
|
self.cellStyleXfs.xf.append(style.as_xf())
|
||
|
|
||
|
|
||
|
@property
|
||
|
def custom_formats(self):
|
||
|
return dict([(n.numFmtId, n.formatCode) for n in self.numFmts.numFmt])
|
||
|
|
||
|
|
||
|
def _normalise_numbers(self):
|
||
|
"""
|
||
|
Rebase custom numFmtIds with a floor of 164 when reading stylesheet
|
||
|
And index datetime formats
|
||
|
"""
|
||
|
date_formats = set()
|
||
|
timedelta_formats = set()
|
||
|
custom = self.custom_formats
|
||
|
formats = self.number_formats
|
||
|
for idx, style in enumerate(self.cell_styles):
|
||
|
if style.numFmtId in custom:
|
||
|
fmt = custom[style.numFmtId]
|
||
|
if fmt in BUILTIN_FORMATS_REVERSE: # remove builtins
|
||
|
style.numFmtId = BUILTIN_FORMATS_REVERSE[fmt]
|
||
|
else:
|
||
|
style.numFmtId = formats.add(fmt) + BUILTIN_FORMATS_MAX_SIZE
|
||
|
else:
|
||
|
fmt = builtin_format_code(style.numFmtId)
|
||
|
if is_date_format(fmt):
|
||
|
# Create an index of which styles refer to datetimes
|
||
|
date_formats.add(idx)
|
||
|
if is_timedelta_format(fmt):
|
||
|
# Create an index of which styles refer to timedeltas
|
||
|
timedelta_formats.add(idx)
|
||
|
self.date_formats = date_formats
|
||
|
self.timedelta_formats = timedelta_formats
|
||
|
|
||
|
|
||
|
def to_tree(self, tagname=None, idx=None, namespace=None):
|
||
|
tree = super().to_tree(tagname, idx, namespace)
|
||
|
tree.set("xmlns", SHEET_MAIN_NS)
|
||
|
return tree
|
||
|
|
||
|
|
||
|
def apply_stylesheet(archive, wb):
|
||
|
"""
|
||
|
Add styles to workbook if present
|
||
|
"""
|
||
|
try:
|
||
|
src = archive.read(ARC_STYLE)
|
||
|
except KeyError:
|
||
|
return wb
|
||
|
|
||
|
node = fromstring(src)
|
||
|
stylesheet = Stylesheet.from_tree(node)
|
||
|
|
||
|
if stylesheet.cell_styles:
|
||
|
|
||
|
wb._borders = IndexedList(stylesheet.borders)
|
||
|
wb._fonts = IndexedList(stylesheet.fonts)
|
||
|
wb._fills = IndexedList(stylesheet.fills)
|
||
|
wb._differential_styles.styles = stylesheet.dxfs
|
||
|
wb._number_formats = stylesheet.number_formats
|
||
|
wb._protections = stylesheet.protections
|
||
|
wb._alignments = stylesheet.alignments
|
||
|
wb._table_styles = stylesheet.tableStyles
|
||
|
|
||
|
# need to overwrite openpyxl defaults in case workbook has different ones
|
||
|
wb._cell_styles = stylesheet.cell_styles
|
||
|
wb._named_styles = stylesheet.named_styles
|
||
|
wb._date_formats = stylesheet.date_formats
|
||
|
wb._timedelta_formats = stylesheet.timedelta_formats
|
||
|
|
||
|
for ns in wb._named_styles:
|
||
|
ns.bind(wb)
|
||
|
|
||
|
else:
|
||
|
warn("Workbook contains no stylesheet, using openpyxl's defaults")
|
||
|
|
||
|
if not wb._named_styles:
|
||
|
normal = styles['Normal']
|
||
|
wb.add_named_style(normal)
|
||
|
warn("Workbook contains no default style, apply openpyxl's default")
|
||
|
|
||
|
if stylesheet.colors is not None:
|
||
|
wb._colors = stylesheet.colors.index
|
||
|
|
||
|
|
||
|
def write_stylesheet(wb):
|
||
|
stylesheet = Stylesheet()
|
||
|
stylesheet.fonts = wb._fonts
|
||
|
stylesheet.fills = wb._fills
|
||
|
stylesheet.borders = wb._borders
|
||
|
stylesheet.dxfs = wb._differential_styles.styles
|
||
|
stylesheet.colors = ColorList(indexedColors=wb._colors)
|
||
|
|
||
|
from .numbers import NumberFormat
|
||
|
fmts = []
|
||
|
for idx, code in enumerate(wb._number_formats, BUILTIN_FORMATS_MAX_SIZE):
|
||
|
fmt = NumberFormat(idx, code)
|
||
|
fmts.append(fmt)
|
||
|
|
||
|
stylesheet.numFmts.numFmt = fmts
|
||
|
|
||
|
xfs = []
|
||
|
for style in wb._cell_styles:
|
||
|
xf = CellStyle.from_array(style)
|
||
|
|
||
|
if style.alignmentId:
|
||
|
xf.alignment = wb._alignments[style.alignmentId]
|
||
|
|
||
|
if style.protectionId:
|
||
|
xf.protection = wb._protections[style.protectionId]
|
||
|
xfs.append(xf)
|
||
|
stylesheet.cellXfs = CellStyleList(xf=xfs)
|
||
|
|
||
|
stylesheet._split_named_styles(wb)
|
||
|
stylesheet.tableStyles = wb._table_styles
|
||
|
|
||
|
return stylesheet.to_tree()
|