Updated script that can be controled by Nodejs web app

This commit is contained in:
mac OS
2024-11-25 12:24:18 +07:00
parent c440eda1f4
commit 8b0ab2bd3a
8662 changed files with 1803808 additions and 34 deletions

View File

@ -0,0 +1,11 @@
# Copyright (c) 2010-2024 openpyxl
from .alignment import Alignment
from .borders import Border, Side
from .colors import Color
from .fills import PatternFill, GradientFill, Fill
from .fonts import Font, DEFAULT_FONT
from .numbers import NumberFormatDescriptor, is_date_format, is_builtin
from .protection import Protection
from .named_styles import NamedStyle

View File

@ -0,0 +1,62 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.compat import safe_string
from openpyxl.descriptors import Bool, MinMax, Min, Alias, NoneSet
from openpyxl.descriptors.serialisable import Serialisable
horizontal_alignments = (
"general", "left", "center", "right", "fill", "justify", "centerContinuous",
"distributed", )
vertical_aligments = (
"top", "center", "bottom", "justify", "distributed",
)
class Alignment(Serialisable):
"""Alignment options for use in styles."""
tagname = "alignment"
horizontal = NoneSet(values=horizontal_alignments)
vertical = NoneSet(values=vertical_aligments)
textRotation = NoneSet(values=range(181))
textRotation.values.add(255)
text_rotation = Alias('textRotation')
wrapText = Bool(allow_none=True)
wrap_text = Alias('wrapText')
shrinkToFit = Bool(allow_none=True)
shrink_to_fit = Alias('shrinkToFit')
indent = MinMax(min=0, max=255)
relativeIndent = MinMax(min=-255, max=255)
justifyLastLine = Bool(allow_none=True)
readingOrder = Min(min=0)
def __init__(self, horizontal=None, vertical=None,
textRotation=0, wrapText=None, shrinkToFit=None, indent=0, relativeIndent=0,
justifyLastLine=None, readingOrder=0, text_rotation=None,
wrap_text=None, shrink_to_fit=None, mergeCell=None):
self.horizontal = horizontal
self.vertical = vertical
self.indent = indent
self.relativeIndent = relativeIndent
self.justifyLastLine = justifyLastLine
self.readingOrder = readingOrder
if text_rotation is not None:
textRotation = text_rotation
if textRotation is not None:
self.textRotation = int(textRotation)
if wrap_text is not None:
wrapText = wrap_text
self.wrapText = wrapText
if shrink_to_fit is not None:
shrinkToFit = shrink_to_fit
self.shrinkToFit = shrinkToFit
# mergeCell is vestigial
def __iter__(self):
for attr in self.__attrs__:
value = getattr(self, attr)
if value is not None and value != 0:
yield attr, safe_string(value)

View File

@ -0,0 +1,103 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.compat import safe_string
from openpyxl.descriptors import (
NoneSet,
Typed,
Bool,
Alias,
Sequence,
Integer,
)
from openpyxl.descriptors.serialisable import Serialisable
from .colors import ColorDescriptor
BORDER_NONE = None
BORDER_DASHDOT = 'dashDot'
BORDER_DASHDOTDOT = 'dashDotDot'
BORDER_DASHED = 'dashed'
BORDER_DOTTED = 'dotted'
BORDER_DOUBLE = 'double'
BORDER_HAIR = 'hair'
BORDER_MEDIUM = 'medium'
BORDER_MEDIUMDASHDOT = 'mediumDashDot'
BORDER_MEDIUMDASHDOTDOT = 'mediumDashDotDot'
BORDER_MEDIUMDASHED = 'mediumDashed'
BORDER_SLANTDASHDOT = 'slantDashDot'
BORDER_THICK = 'thick'
BORDER_THIN = 'thin'
class Side(Serialisable):
"""Border options for use in styles.
Caution: if you do not specify a border_style, other attributes will
have no effect !"""
color = ColorDescriptor(allow_none=True)
style = NoneSet(values=('dashDot','dashDotDot', 'dashed','dotted',
'double','hair', 'medium', 'mediumDashDot', 'mediumDashDotDot',
'mediumDashed', 'slantDashDot', 'thick', 'thin')
)
border_style = Alias('style')
def __init__(self, style=None, color=None, border_style=None):
if border_style is not None:
style = border_style
self.style = style
self.color = color
class Border(Serialisable):
"""Border positioning for use in styles."""
tagname = "border"
__elements__ = ('start', 'end', 'left', 'right', 'top', 'bottom',
'diagonal', 'vertical', 'horizontal')
# child elements
start = Typed(expected_type=Side, allow_none=True)
end = Typed(expected_type=Side, allow_none=True)
left = Typed(expected_type=Side, allow_none=True)
right = Typed(expected_type=Side, allow_none=True)
top = Typed(expected_type=Side, allow_none=True)
bottom = Typed(expected_type=Side, allow_none=True)
diagonal = Typed(expected_type=Side, allow_none=True)
vertical = Typed(expected_type=Side, allow_none=True)
horizontal = Typed(expected_type=Side, allow_none=True)
# attributes
outline = Bool()
diagonalUp = Bool()
diagonalDown = Bool()
def __init__(self, left=None, right=None, top=None,
bottom=None, diagonal=None, diagonal_direction=None,
vertical=None, horizontal=None, diagonalUp=False, diagonalDown=False,
outline=True, start=None, end=None):
self.left = left
self.right = right
self.top = top
self.bottom = bottom
self.diagonal = diagonal
self.vertical = vertical
self.horizontal = horizontal
self.diagonal_direction = diagonal_direction
self.diagonalUp = diagonalUp
self.diagonalDown = diagonalDown
self.outline = outline
self.start = start
self.end = end
def __iter__(self):
for attr in self.__attrs__:
value = getattr(self, attr)
if value and attr != "outline":
yield attr, safe_string(value)
elif attr == "outline" and not value:
yield attr, safe_string(value)
DEFAULT_BORDER = Border(left=Side(), right=Side(), top=Side(), bottom=Side(), diagonal=Side())

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,206 @@
# Copyright (c) 2010-2024 openpyxl
from array import array
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors import (
Typed,
Float,
Bool,
Integer,
Sequence,
)
from openpyxl.descriptors.excel import ExtensionList
from openpyxl.utils.indexed_list import IndexedList
from .alignment import Alignment
from .protection import Protection
class ArrayDescriptor:
def __init__(self, key):
self.key = key
def __get__(self, instance, cls):
return instance[self.key]
def __set__(self, instance, value):
instance[self.key] = value
class StyleArray(array):
"""
Simplified named tuple with an array
"""
__slots__ = ()
tagname = 'xf'
fontId = ArrayDescriptor(0)
fillId = ArrayDescriptor(1)
borderId = ArrayDescriptor(2)
numFmtId = ArrayDescriptor(3)
protectionId = ArrayDescriptor(4)
alignmentId = ArrayDescriptor(5)
pivotButton = ArrayDescriptor(6)
quotePrefix = ArrayDescriptor(7)
xfId = ArrayDescriptor(8)
def __new__(cls, args=[0]*9):
return array.__new__(cls, 'i', args)
def __hash__(self):
return hash(tuple(self))
def __copy__(self):
return StyleArray((self))
def __deepcopy__(self, memo):
return StyleArray((self))
class CellStyle(Serialisable):
tagname = "xf"
numFmtId = Integer()
fontId = Integer()
fillId = Integer()
borderId = Integer()
xfId = Integer(allow_none=True)
quotePrefix = Bool(allow_none=True)
pivotButton = Bool(allow_none=True)
applyNumberFormat = Bool(allow_none=True)
applyFont = Bool(allow_none=True)
applyFill = Bool(allow_none=True)
applyBorder = Bool(allow_none=True)
applyAlignment = Bool(allow_none=True)
applyProtection = Bool(allow_none=True)
alignment = Typed(expected_type=Alignment, allow_none=True)
protection = Typed(expected_type=Protection, allow_none=True)
extLst = Typed(expected_type=ExtensionList, allow_none=True)
__elements__ = ('alignment', 'protection')
__attrs__ = ("numFmtId", "fontId", "fillId", "borderId",
"applyAlignment", "applyProtection", "pivotButton", "quotePrefix", "xfId")
def __init__(self,
numFmtId=0,
fontId=0,
fillId=0,
borderId=0,
xfId=None,
quotePrefix=None,
pivotButton=None,
applyNumberFormat=None,
applyFont=None,
applyFill=None,
applyBorder=None,
applyAlignment=None,
applyProtection=None,
alignment=None,
protection=None,
extLst=None,
):
self.numFmtId = numFmtId
self.fontId = fontId
self.fillId = fillId
self.borderId = borderId
self.xfId = xfId
self.quotePrefix = quotePrefix
self.pivotButton = pivotButton
self.applyNumberFormat = applyNumberFormat
self.applyFont = applyFont
self.applyFill = applyFill
self.applyBorder = applyBorder
self.alignment = alignment
self.protection = protection
def to_array(self):
"""
Convert to StyleArray
"""
style = StyleArray()
for k in ("fontId", "fillId", "borderId", "numFmtId", "pivotButton",
"quotePrefix", "xfId"):
v = getattr(self, k, 0)
if v is not None:
setattr(style, k, v)
return style
@classmethod
def from_array(cls, style):
"""
Convert from StyleArray
"""
return cls(numFmtId=style.numFmtId, fontId=style.fontId,
fillId=style.fillId, borderId=style.borderId, xfId=style.xfId,
quotePrefix=style.quotePrefix, pivotButton=style.pivotButton,)
@property
def applyProtection(self):
return self.protection is not None or None
@property
def applyAlignment(self):
return self.alignment is not None or None
class CellStyleList(Serialisable):
tagname = "cellXfs"
__attrs__ = ("count",)
count = Integer(allow_none=True)
xf = Sequence(expected_type=CellStyle)
alignment = Sequence(expected_type=Alignment)
protection = Sequence(expected_type=Protection)
__elements__ = ('xf',)
def __init__(self,
count=None,
xf=(),
):
self.xf = xf
@property
def count(self):
return len(self.xf)
def __getitem__(self, idx):
try:
return self.xf[idx]
except IndexError:
print((f"{idx} is out of range"))
return self.xf[idx]
def _to_array(self):
"""
Extract protection and alignments, convert to style array
"""
self.prots = IndexedList([Protection()])
self.alignments = IndexedList([Alignment()])
styles = [] # allow duplicates
for xf in self.xf:
style = xf.to_array()
if xf.alignment is not None:
style.alignmentId = self.alignments.add(xf.alignment)
if xf.protection is not None:
style.protectionId = self.prots.add(xf.protection)
styles.append(style)
return IndexedList(styles)

View File

@ -0,0 +1,172 @@
# Copyright (c) 2010-2024 openpyxl
import re
from openpyxl.compat import safe_string
from openpyxl.descriptors import (
String,
Bool,
MinMax,
Integer,
Typed,
)
from openpyxl.descriptors.sequence import NestedSequence
from openpyxl.descriptors.serialisable import Serialisable
# Default Color Index as per 18.8.27 of ECMA Part 4
COLOR_INDEX = (
'00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', #0-4
'00FFFF00', '00FF00FF', '0000FFFF', '00000000', '00FFFFFF', #5-9
'00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', #10-14
'0000FFFF', '00800000', '00008000', '00000080', '00808000', #15-19
'00800080', '00008080', '00C0C0C0', '00808080', '009999FF', #20-24
'00993366', '00FFFFCC', '00CCFFFF', '00660066', '00FF8080', #25-29
'000066CC', '00CCCCFF', '00000080', '00FF00FF', '00FFFF00', #30-34
'0000FFFF', '00800080', '00800000', '00008080', '000000FF', #35-39
'0000CCFF', '00CCFFFF', '00CCFFCC', '00FFFF99', '0099CCFF', #40-44
'00FF99CC', '00CC99FF', '00FFCC99', '003366FF', '0033CCCC', #45-49
'0099CC00', '00FFCC00', '00FF9900', '00FF6600', '00666699', #50-54
'00969696', '00003366', '00339966', '00003300', '00333300', #55-59
'00993300', '00993366', '00333399', '00333333', #60-63
)
# indices 64 and 65 are reserved for the system foreground and background colours respectively
# Will remove these definitions in a future release
BLACK = COLOR_INDEX[0]
WHITE = COLOR_INDEX[1]
#RED = COLOR_INDEX[2]
#DARKRED = COLOR_INDEX[8]
BLUE = COLOR_INDEX[4]
#DARKBLUE = COLOR_INDEX[12]
#GREEN = COLOR_INDEX[3]
#DARKGREEN = COLOR_INDEX[9]
#YELLOW = COLOR_INDEX[5]
#DARKYELLOW = COLOR_INDEX[19]
aRGB_REGEX = re.compile("^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6})$")
class RGB(Typed):
"""
Descriptor for aRGB values
If not supplied alpha is 00
"""
expected_type = str
def __set__(self, instance, value):
if not self.allow_none:
m = aRGB_REGEX.match(value)
if m is None:
raise ValueError("Colors must be aRGB hex values")
if len(value) == 6:
value = "00" + value
super().__set__(instance, value)
class Color(Serialisable):
"""Named colors for use in styles."""
tagname = "color"
rgb = RGB()
indexed = Integer()
auto = Bool()
theme = Integer()
tint = MinMax(min=-1, max=1, expected_type=float)
type = String()
def __init__(self, rgb=BLACK, indexed=None, auto=None, theme=None, tint=0.0, index=None, type='rgb'):
if index is not None:
indexed = index
if indexed is not None:
self.type = 'indexed'
self.indexed = indexed
elif theme is not None:
self.type = 'theme'
self.theme = theme
elif auto is not None:
self.type = 'auto'
self.auto = auto
else:
self.rgb = rgb
self.type = 'rgb'
self.tint = tint
@property
def value(self):
return getattr(self, self.type)
@value.setter
def value(self, value):
setattr(self, self.type, value)
def __iter__(self):
attrs = [(self.type, self.value)]
if self.tint != 0:
attrs.append(('tint', self.tint))
for k, v in attrs:
yield k, safe_string(v)
@property
def index(self):
# legacy
return self.value
def __add__(self, other):
"""
Adding colours is undefined behaviour best do nothing
"""
if not isinstance(other, Color):
return super().__add__(other)
return self
class ColorDescriptor(Typed):
expected_type = Color
def __set__(self, instance, value):
if isinstance(value, str):
value = Color(rgb=value)
super().__set__(instance, value)
class RgbColor(Serialisable):
tagname = "rgbColor"
rgb = RGB()
def __init__(self,
rgb=None,
):
self.rgb = rgb
class ColorList(Serialisable):
tagname = "colors"
indexedColors = NestedSequence(expected_type=RgbColor)
mruColors = NestedSequence(expected_type=Color)
__elements__ = ('indexedColors', 'mruColors')
def __init__(self,
indexedColors=(),
mruColors=(),
):
self.indexedColors = indexedColors
self.mruColors = mruColors
def __bool__(self):
return bool(self.indexedColors) or bool(self.mruColors)
@property
def index(self):
return [val.rgb for val in self.indexedColors]

View File

@ -0,0 +1,95 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.descriptors import (
Typed,
Sequence,
Alias,
)
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.styles import (
Font,
Fill,
Border,
Alignment,
Protection,
)
from .numbers import NumberFormat
class DifferentialStyle(Serialisable):
tagname = "dxf"
__elements__ = ("font", "numFmt", "fill", "alignment", "border", "protection")
font = Typed(expected_type=Font, allow_none=True)
numFmt = Typed(expected_type=NumberFormat, allow_none=True)
fill = Typed(expected_type=Fill, allow_none=True)
alignment = Typed(expected_type=Alignment, allow_none=True)
border = Typed(expected_type=Border, allow_none=True)
protection = Typed(expected_type=Protection, allow_none=True)
def __init__(self,
font=None,
numFmt=None,
fill=None,
alignment=None,
border=None,
protection=None,
extLst=None,
):
self.font = font
self.numFmt = numFmt
self.fill = fill
self.alignment = alignment
self.border = border
self.protection = protection
self.extLst = extLst
class DifferentialStyleList(Serialisable):
"""
Dedupable container for differential styles.
"""
tagname = "dxfs"
dxf = Sequence(expected_type=DifferentialStyle)
styles = Alias("dxf")
__attrs__ = ("count",)
def __init__(self, dxf=(), count=None):
self.dxf = dxf
def append(self, dxf):
"""
Check to see whether style already exists and append it if does not.
"""
if not isinstance(dxf, DifferentialStyle):
raise TypeError('expected ' + str(DifferentialStyle))
if dxf in self.styles:
return
self.styles.append(dxf)
def add(self, dxf):
"""
Add a differential style and return its index
"""
self.append(dxf)
return self.styles.index(dxf)
def __bool__(self):
return bool(self.styles)
def __getitem__(self, idx):
return self.styles[idx]
@property
def count(self):
return len(self.dxf)

View File

@ -0,0 +1,224 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.descriptors import (
Float,
Set,
Alias,
NoneSet,
Sequence,
Integer,
MinMax,
)
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.compat import safe_string
from .colors import ColorDescriptor, Color
from openpyxl.xml.functions import Element, localname
from openpyxl.xml.constants import SHEET_MAIN_NS
FILL_NONE = 'none'
FILL_SOLID = 'solid'
FILL_PATTERN_DARKDOWN = 'darkDown'
FILL_PATTERN_DARKGRAY = 'darkGray'
FILL_PATTERN_DARKGRID = 'darkGrid'
FILL_PATTERN_DARKHORIZONTAL = 'darkHorizontal'
FILL_PATTERN_DARKTRELLIS = 'darkTrellis'
FILL_PATTERN_DARKUP = 'darkUp'
FILL_PATTERN_DARKVERTICAL = 'darkVertical'
FILL_PATTERN_GRAY0625 = 'gray0625'
FILL_PATTERN_GRAY125 = 'gray125'
FILL_PATTERN_LIGHTDOWN = 'lightDown'
FILL_PATTERN_LIGHTGRAY = 'lightGray'
FILL_PATTERN_LIGHTGRID = 'lightGrid'
FILL_PATTERN_LIGHTHORIZONTAL = 'lightHorizontal'
FILL_PATTERN_LIGHTTRELLIS = 'lightTrellis'
FILL_PATTERN_LIGHTUP = 'lightUp'
FILL_PATTERN_LIGHTVERTICAL = 'lightVertical'
FILL_PATTERN_MEDIUMGRAY = 'mediumGray'
fills = (FILL_SOLID, FILL_PATTERN_DARKDOWN, FILL_PATTERN_DARKGRAY,
FILL_PATTERN_DARKGRID, FILL_PATTERN_DARKHORIZONTAL, FILL_PATTERN_DARKTRELLIS,
FILL_PATTERN_DARKUP, FILL_PATTERN_DARKVERTICAL, FILL_PATTERN_GRAY0625,
FILL_PATTERN_GRAY125, FILL_PATTERN_LIGHTDOWN, FILL_PATTERN_LIGHTGRAY,
FILL_PATTERN_LIGHTGRID, FILL_PATTERN_LIGHTHORIZONTAL,
FILL_PATTERN_LIGHTTRELLIS, FILL_PATTERN_LIGHTUP, FILL_PATTERN_LIGHTVERTICAL,
FILL_PATTERN_MEDIUMGRAY)
class Fill(Serialisable):
"""Base class"""
tagname = "fill"
@classmethod
def from_tree(cls, el):
children = [c for c in el]
if not children:
return
child = children[0]
if "patternFill" in child.tag:
return PatternFill._from_tree(child)
return super(Fill, GradientFill).from_tree(child)
class PatternFill(Fill):
"""Area fill patterns for use in styles.
Caution: if you do not specify a fill_type, other attributes will have
no effect !"""
tagname = "patternFill"
__elements__ = ('fgColor', 'bgColor')
patternType = NoneSet(values=fills)
fill_type = Alias("patternType")
fgColor = ColorDescriptor()
start_color = Alias("fgColor")
bgColor = ColorDescriptor()
end_color = Alias("bgColor")
def __init__(self, patternType=None, fgColor=Color(), bgColor=Color(),
fill_type=None, start_color=None, end_color=None):
if fill_type is not None:
patternType = fill_type
self.patternType = patternType
if start_color is not None:
fgColor = start_color
self.fgColor = fgColor
if end_color is not None:
bgColor = end_color
self.bgColor = bgColor
@classmethod
def _from_tree(cls, el):
attrib = dict(el.attrib)
for child in el:
desc = localname(child)
attrib[desc] = Color.from_tree(child)
return cls(**attrib)
def to_tree(self, tagname=None, idx=None):
parent = Element("fill")
el = Element(self.tagname)
if self.patternType is not None:
el.set('patternType', self.patternType)
for c in self.__elements__:
value = getattr(self, c)
if value != Color():
el.append(value.to_tree(c))
parent.append(el)
return parent
DEFAULT_EMPTY_FILL = PatternFill()
DEFAULT_GRAY_FILL = PatternFill(patternType='gray125')
class Stop(Serialisable):
tagname = "stop"
position = MinMax(min=0, max=1)
color = ColorDescriptor()
def __init__(self, color, position):
self.position = position
self.color = color
def _assign_position(values):
"""
Automatically assign positions if a list of colours is provided.
It is not permitted to mix colours and stops
"""
n_values = len(values)
n_stops = sum(isinstance(value, Stop) for value in values)
if n_stops == 0:
interval = 1
if n_values > 2:
interval = 1 / (n_values - 1)
values = [Stop(value, i * interval)
for i, value in enumerate(values)]
elif n_stops < n_values:
raise ValueError('Cannot interpret mix of Stops and Colors in GradientFill')
pos = set()
for stop in values:
if stop.position in pos:
raise ValueError("Duplicate position {0}".format(stop.position))
pos.add(stop.position)
return values
class StopList(Sequence):
expected_type = Stop
def __set__(self, obj, values):
values = _assign_position(values)
super().__set__(obj, values)
class GradientFill(Fill):
"""Fill areas with gradient
Two types of gradient fill are supported:
- A type='linear' gradient interpolates colours between
a set of specified Stops, across the length of an area.
The gradient is left-to-right by default, but this
orientation can be modified with the degree
attribute. A list of Colors can be provided instead
and they will be positioned with equal distance between them.
- A type='path' gradient applies a linear gradient from each
edge of the area. Attributes top, right, bottom, left specify
the extent of fill from the respective borders. Thus top="0.2"
will fill the top 20% of the cell.
"""
tagname = "gradientFill"
type = Set(values=('linear', 'path'))
fill_type = Alias("type")
degree = Float()
left = Float()
right = Float()
top = Float()
bottom = Float()
stop = StopList()
def __init__(self, type="linear", degree=0, left=0, right=0, top=0,
bottom=0, stop=()):
self.degree = degree
self.left = left
self.right = right
self.top = top
self.bottom = bottom
self.stop = stop
self.type = type
def __iter__(self):
for attr in self.__attrs__:
value = getattr(self, attr)
if value:
yield attr, safe_string(value)
def to_tree(self, tagname=None, namespace=None, idx=None):
parent = Element("fill")
el = super().to_tree()
parent.append(el)
return parent

View File

@ -0,0 +1,113 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.descriptors import (
Alias,
Sequence,
Integer
)
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors.nested import (
NestedValue,
NestedBool,
NestedNoneSet,
NestedMinMax,
NestedString,
NestedInteger,
NestedFloat,
)
from .colors import ColorDescriptor, Color, BLACK
from openpyxl.compat import safe_string
from openpyxl.xml.functions import Element, SubElement
from openpyxl.xml.constants import SHEET_MAIN_NS
def _no_value(tagname, value, namespace=None):
if value:
return Element(tagname, val=safe_string(value))
class Font(Serialisable):
"""Font options used in styles."""
UNDERLINE_DOUBLE = 'double'
UNDERLINE_DOUBLE_ACCOUNTING = 'doubleAccounting'
UNDERLINE_SINGLE = 'single'
UNDERLINE_SINGLE_ACCOUNTING = 'singleAccounting'
name = NestedString(allow_none=True)
charset = NestedInteger(allow_none=True)
family = NestedMinMax(min=0, max=14, allow_none=True)
sz = NestedFloat(allow_none=True)
size = Alias("sz")
b = NestedBool(to_tree=_no_value)
bold = Alias("b")
i = NestedBool(to_tree=_no_value)
italic = Alias("i")
strike = NestedBool(allow_none=True)
strikethrough = Alias("strike")
outline = NestedBool(allow_none=True)
shadow = NestedBool(allow_none=True)
condense = NestedBool(allow_none=True)
extend = NestedBool(allow_none=True)
u = NestedNoneSet(values=('single', 'double', 'singleAccounting',
'doubleAccounting'))
underline = Alias("u")
vertAlign = NestedNoneSet(values=('superscript', 'subscript', 'baseline'))
color = ColorDescriptor(allow_none=True)
scheme = NestedNoneSet(values=("major", "minor"))
tagname = "font"
__elements__ = ('name', 'charset', 'family', 'b', 'i', 'strike', 'outline',
'shadow', 'condense', 'color', 'extend', 'sz', 'u', 'vertAlign',
'scheme')
def __init__(self, name=None, sz=None, b=None, i=None, charset=None,
u=None, strike=None, color=None, scheme=None, family=None, size=None,
bold=None, italic=None, strikethrough=None, underline=None,
vertAlign=None, outline=None, shadow=None, condense=None,
extend=None):
self.name = name
self.family = family
if size is not None:
sz = size
self.sz = sz
if bold is not None:
b = bold
self.b = b
if italic is not None:
i = italic
self.i = i
if underline is not None:
u = underline
self.u = u
if strikethrough is not None:
strike = strikethrough
self.strike = strike
self.color = color
self.vertAlign = vertAlign
self.charset = charset
self.outline = outline
self.shadow = shadow
self.condense = condense
self.extend = extend
self.scheme = scheme
@classmethod
def from_tree(cls, node):
"""
Set default value for underline if child element is present
"""
underline = node.find("{%s}u" % SHEET_MAIN_NS)
if underline is not None and underline.get('val') is None:
underline.set("val", "single")
return super().from_tree(node)
DEFAULT_FONT = Font(name="Calibri", sz=11, family=2, b=False, i=False,
color=Color(theme=1), scheme="minor")

View File

@ -0,0 +1,282 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.compat import safe_string
from openpyxl.descriptors import (
Typed,
Integer,
Bool,
String,
Sequence,
)
from openpyxl.descriptors.excel import ExtensionList
from openpyxl.descriptors.serialisable import Serialisable
from .fills import PatternFill, Fill
from .fonts import Font
from .borders import Border
from .alignment import Alignment
from .protection import Protection
from .numbers import (
NumberFormatDescriptor,
BUILTIN_FORMATS_MAX_SIZE,
BUILTIN_FORMATS_REVERSE,
)
from .cell_style import (
StyleArray,
CellStyle,
)
class NamedStyle(Serialisable):
"""
Named and editable styles
"""
font = Typed(expected_type=Font)
fill = Typed(expected_type=Fill)
border = Typed(expected_type=Border)
alignment = Typed(expected_type=Alignment)
number_format = NumberFormatDescriptor()
protection = Typed(expected_type=Protection)
builtinId = Integer(allow_none=True)
hidden = Bool(allow_none=True)
name = String()
_wb = None
_style = StyleArray()
def __init__(self,
name="Normal",
font=None,
fill=None,
border=None,
alignment=None,
number_format=None,
protection=None,
builtinId=None,
hidden=False,
):
self.name = name
self.font = font or Font()
self.fill = fill or PatternFill()
self.border = border or Border()
self.alignment = alignment or Alignment()
self.number_format = number_format
self.protection = protection or Protection()
self.builtinId = builtinId
self.hidden = hidden
self._wb = None
self._style = StyleArray()
def __setattr__(self, attr, value):
super().__setattr__(attr, value)
if getattr(self, '_wb', None) and attr in (
'font', 'fill', 'border', 'alignment', 'number_format', 'protection',
):
self._recalculate()
def __iter__(self):
for key in ('name', 'builtinId', 'hidden', 'xfId'):
value = getattr(self, key, None)
if value is not None:
yield key, safe_string(value)
def bind(self, wb):
"""
Bind a named style to a workbook
"""
self._wb = wb
self._recalculate()
def _recalculate(self):
self._style.fontId = self._wb._fonts.add(self.font)
self._style.borderId = self._wb._borders.add(self.border)
self._style.fillId = self._wb._fills.add(self.fill)
self._style.protectionId = self._wb._protections.add(self.protection)
self._style.alignmentId = self._wb._alignments.add(self.alignment)
fmt = self.number_format
if fmt in BUILTIN_FORMATS_REVERSE:
fmt = BUILTIN_FORMATS_REVERSE[fmt]
else:
fmt = self._wb._number_formats.add(self.number_format) + (
BUILTIN_FORMATS_MAX_SIZE)
self._style.numFmtId = fmt
def as_tuple(self):
"""Return a style array representing the current style"""
return self._style
def as_xf(self):
"""
Return equivalent XfStyle
"""
xf = CellStyle.from_array(self._style)
xf.xfId = None
xf.pivotButton = None
xf.quotePrefix = None
if self.alignment != Alignment():
xf.alignment = self.alignment
if self.protection != Protection():
xf.protection = self.protection
return xf
def as_name(self):
"""
Return relevant named style
"""
named = _NamedCellStyle(
name=self.name,
builtinId=self.builtinId,
hidden=self.hidden,
xfId=self._style.xfId
)
return named
class NamedStyleList(list):
"""
Named styles are editable and can be applied to multiple objects
As only the index is stored in referencing objects the order mus
be preserved.
Returns a list of NamedStyles
"""
def __init__(self, iterable=()):
"""
Allow a list of named styles to be passed in and index them.
"""
for idx, s in enumerate(iterable, len(self)):
s._style.xfId = idx
super().__init__(iterable)
@property
def names(self):
return [s.name for s in self]
def __getitem__(self, key):
if isinstance(key, int):
return super().__getitem__(key)
for idx, name in enumerate(self.names):
if name == key:
return self[idx]
raise KeyError("No named style with the name{0} exists".format(key))
def append(self, style):
if not isinstance(style, NamedStyle):
raise TypeError("""Only NamedStyle instances can be added""")
elif style.name in self.names: # hotspot
raise ValueError("""Style {0} exists already""".format(style.name))
style._style.xfId = (len(self))
super().append(style)
class _NamedCellStyle(Serialisable):
"""
Pointer-based representation of named styles in XML
xfId refers to the corresponding CellStyleXfs
Not used in client code.
"""
tagname = "cellStyle"
name = String()
xfId = Integer()
builtinId = Integer(allow_none=True)
iLevel = Integer(allow_none=True)
hidden = Bool(allow_none=True)
customBuiltin = Bool(allow_none=True)
extLst = Typed(expected_type=ExtensionList, allow_none=True)
__elements__ = ()
def __init__(self,
name=None,
xfId=None,
builtinId=None,
iLevel=None,
hidden=None,
customBuiltin=None,
extLst=None,
):
self.name = name
self.xfId = xfId
self.builtinId = builtinId
self.iLevel = iLevel
self.hidden = hidden
self.customBuiltin = customBuiltin
class _NamedCellStyleList(Serialisable):
"""
Container for named cell style objects
Not used in client code
"""
tagname = "cellStyles"
count = Integer(allow_none=True)
cellStyle = Sequence(expected_type=_NamedCellStyle)
__attrs__ = ("count",)
def __init__(self,
count=None,
cellStyle=(),
):
self.cellStyle = cellStyle
@property
def count(self):
return len(self.cellStyle)
def remove_duplicates(self):
"""
Some applications contain duplicate definitions either by name or
referenced style.
As the references are 0-based indices, styles are sorted by
index.
Returns a list of style references with duplicates removed
"""
def sort_fn(v):
return v.xfId
styles = []
names = set()
ids = set()
for ns in sorted(self.cellStyle, key=sort_fn):
if ns.xfId in ids or ns.name in names: # skip duplicates
continue
ids.add(ns.xfId)
names.add(ns.name)
styles.append(ns)
return styles

View File

@ -0,0 +1,200 @@
# Copyright (c) 2010-2024 openpyxl
import re
from openpyxl.descriptors import (
String,
Sequence,
Integer,
)
from openpyxl.descriptors.serialisable import Serialisable
BUILTIN_FORMATS = {
0: 'General',
1: '0',
2: '0.00',
3: '#,##0',
4: '#,##0.00',
5: '"$"#,##0_);("$"#,##0)',
6: '"$"#,##0_);[Red]("$"#,##0)',
7: '"$"#,##0.00_);("$"#,##0.00)',
8: '"$"#,##0.00_);[Red]("$"#,##0.00)',
9: '0%',
10: '0.00%',
11: '0.00E+00',
12: '# ?/?',
13: '# ??/??',
14: 'mm-dd-yy',
15: 'd-mmm-yy',
16: 'd-mmm',
17: 'mmm-yy',
18: 'h:mm AM/PM',
19: 'h:mm:ss AM/PM',
20: 'h:mm',
21: 'h:mm:ss',
22: 'm/d/yy h:mm',
37: '#,##0_);(#,##0)',
38: '#,##0_);[Red](#,##0)',
39: '#,##0.00_);(#,##0.00)',
40: '#,##0.00_);[Red](#,##0.00)',
41: r'_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)',
42: r'_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)',
43: r'_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)',
44: r'_("$"* #,##0.00_)_("$"* \(#,##0.00\)_("$"* "-"??_)_(@_)',
45: 'mm:ss',
46: '[h]:mm:ss',
47: 'mmss.0',
48: '##0.0E+0',
49: '@', }
BUILTIN_FORMATS_MAX_SIZE = 164
BUILTIN_FORMATS_REVERSE = dict(
[(value, key) for key, value in BUILTIN_FORMATS.items()])
FORMAT_GENERAL = BUILTIN_FORMATS[0]
FORMAT_TEXT = BUILTIN_FORMATS[49]
FORMAT_NUMBER = BUILTIN_FORMATS[1]
FORMAT_NUMBER_00 = BUILTIN_FORMATS[2]
FORMAT_NUMBER_COMMA_SEPARATED1 = BUILTIN_FORMATS[4]
FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-'
FORMAT_PERCENTAGE = BUILTIN_FORMATS[9]
FORMAT_PERCENTAGE_00 = BUILTIN_FORMATS[10]
FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'
FORMAT_DATE_YYMMDD = 'yy-mm-dd'
FORMAT_DATE_DDMMYY = 'dd/mm/yy'
FORMAT_DATE_DMYSLASH = 'd/m/y'
FORMAT_DATE_DMYMINUS = 'd-m-y'
FORMAT_DATE_DMMINUS = 'd-m'
FORMAT_DATE_MYMINUS = 'm-y'
FORMAT_DATE_XLSX14 = BUILTIN_FORMATS[14]
FORMAT_DATE_XLSX15 = BUILTIN_FORMATS[15]
FORMAT_DATE_XLSX16 = BUILTIN_FORMATS[16]
FORMAT_DATE_XLSX17 = BUILTIN_FORMATS[17]
FORMAT_DATE_XLSX22 = BUILTIN_FORMATS[22]
FORMAT_DATE_DATETIME = 'yyyy-mm-dd h:mm:ss'
FORMAT_DATE_TIME1 = BUILTIN_FORMATS[18]
FORMAT_DATE_TIME2 = BUILTIN_FORMATS[19]
FORMAT_DATE_TIME3 = BUILTIN_FORMATS[20]
FORMAT_DATE_TIME4 = BUILTIN_FORMATS[21]
FORMAT_DATE_TIME5 = BUILTIN_FORMATS[45]
FORMAT_DATE_TIME6 = BUILTIN_FORMATS[21]
FORMAT_DATE_TIME7 = 'i:s.S'
FORMAT_DATE_TIME8 = 'h:mm:ss@'
FORMAT_DATE_TIMEDELTA = '[hh]:mm:ss'
FORMAT_DATE_YYMMDDSLASH = 'yy/mm/dd@'
FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-'
FORMAT_CURRENCY_USD = '$#,##0_-'
FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-'
COLORS = r"\[(BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW)\]"
LITERAL_GROUP = r'".*?"' # anything in quotes
LOCALE_GROUP = r'\[(?!hh?\]|mm?\]|ss?\])[^\]]*\]' # anything in square brackets, except hours or minutes or seconds
STRIP_RE = re.compile(f"{LITERAL_GROUP}|{LOCALE_GROUP}")
TIMEDELTA_RE = re.compile(r'\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\](:ss(\.0*)?)?|\[ss?\](\.0*)?', re.I)
# Spec 18.8.31 numFmts
# +ve;-ve;zero;text
def is_date_format(fmt):
if fmt is None:
return False
fmt = fmt.split(";")[0] # only look at the first format
fmt = STRIP_RE.sub("", fmt) # ignore some formats
return re.search(r"(?<![_\\])[dmhysDMHYS]", fmt) is not None
def is_timedelta_format(fmt):
if fmt is None:
return False
fmt = fmt.split(";")[0] # only look at the first format
return TIMEDELTA_RE.search(fmt) is not None
def is_datetime(fmt):
"""
Return date, time or datetime
"""
if not is_date_format(fmt):
return
DATE = TIME = False
if any((x in fmt for x in 'dy')):
DATE = True
if any((x in fmt for x in 'hs')):
TIME = True
if DATE and TIME:
return "datetime"
if DATE:
return "date"
return "time"
def is_builtin(fmt):
return fmt in BUILTIN_FORMATS.values()
def builtin_format_code(index):
"""Return one of the standard format codes by index."""
try:
fmt = BUILTIN_FORMATS[index]
except KeyError:
fmt = None
return fmt
def builtin_format_id(fmt):
"""Return the id of a standard style."""
return BUILTIN_FORMATS_REVERSE.get(fmt)
class NumberFormatDescriptor(String):
def __set__(self, instance, value):
if value is None:
value = FORMAT_GENERAL
super().__set__(instance, value)
class NumberFormat(Serialisable):
numFmtId = Integer()
formatCode = String()
def __init__(self,
numFmtId=None,
formatCode=None,
):
self.numFmtId = numFmtId
self.formatCode = formatCode
class NumberFormatList(Serialisable):
count = Integer(allow_none=True)
numFmt = Sequence(expected_type=NumberFormat)
__elements__ = ('numFmt',)
__attrs__ = ("count",)
def __init__(self,
count=None,
numFmt=(),
):
self.numFmt = numFmt
@property
def count(self):
return len(self.numFmt)
def __getitem__(self, idx):
return self.numFmt[idx]

View File

@ -0,0 +1,17 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.descriptors import Bool
from openpyxl.descriptors.serialisable import Serialisable
class Protection(Serialisable):
"""Protection options for use in styles."""
tagname = "protection"
locked = Bool()
hidden = Bool()
def __init__(self, locked=True, hidden=False):
self.locked = locked
self.hidden = hidden

View File

@ -0,0 +1,62 @@
# Copyright (c) 2010-2024 openpyxl
from copy import copy
from openpyxl.compat import deprecated
class StyleProxy:
"""
Proxy formatting objects so that they cannot be altered
"""
__slots__ = ('__target')
def __init__(self, target):
self.__target = target
def __repr__(self):
return repr(self.__target)
def __getattr__(self, attr):
return getattr(self.__target, attr)
def __setattr__(self, attr, value):
if attr != "_StyleProxy__target":
raise AttributeError("Style objects are immutable and cannot be changed."
"Reassign the style with a copy")
super().__setattr__(attr, value)
def __copy__(self):
"""
Return a copy of the proxied object.
"""
return copy(self.__target)
def __add__(self, other):
"""
Add proxied object to another instance and return the combined object
"""
return self.__target + other
@deprecated("Use copy(obj) or cell.obj = cell.obj + other")
def copy(self, **kw):
"""Return a copy of the proxied object. Keyword args will be passed through"""
cp = copy(self.__target)
for k, v in kw.items():
setattr(cp, k, v)
return cp
def __eq__(self, other):
return self.__target == other
def __ne__(self, other):
return not self == other

View File

@ -0,0 +1,151 @@
# Copyright (c) 2010-2024 openpyxl
from copy import copy
from .numbers import (
BUILTIN_FORMATS,
BUILTIN_FORMATS_MAX_SIZE,
BUILTIN_FORMATS_REVERSE,
)
from .proxy import StyleProxy
from .cell_style import StyleArray
from .named_styles import NamedStyle
from .builtins import styles
class StyleDescriptor:
def __init__(self, collection, key):
self.collection = collection
self.key = key
def __set__(self, instance, value):
coll = getattr(instance.parent.parent, self.collection)
if not getattr(instance, "_style"):
instance._style = StyleArray()
setattr(instance._style, self.key, coll.add(value))
def __get__(self, instance, cls):
coll = getattr(instance.parent.parent, self.collection)
if not getattr(instance, "_style"):
instance._style = StyleArray()
idx = getattr(instance._style, self.key)
return StyleProxy(coll[idx])
class NumberFormatDescriptor:
key = "numFmtId"
collection = '_number_formats'
def __set__(self, instance, value):
coll = getattr(instance.parent.parent, self.collection)
if value in BUILTIN_FORMATS_REVERSE:
idx = BUILTIN_FORMATS_REVERSE[value]
else:
idx = coll.add(value) + BUILTIN_FORMATS_MAX_SIZE
if not getattr(instance, "_style"):
instance._style = StyleArray()
setattr(instance._style, self.key, idx)
def __get__(self, instance, cls):
if not getattr(instance, "_style"):
instance._style = StyleArray()
idx = getattr(instance._style, self.key)
if idx < BUILTIN_FORMATS_MAX_SIZE:
return BUILTIN_FORMATS.get(idx, "General")
coll = getattr(instance.parent.parent, self.collection)
return coll[idx - BUILTIN_FORMATS_MAX_SIZE]
class NamedStyleDescriptor:
key = "xfId"
collection = "_named_styles"
def __set__(self, instance, value):
if not getattr(instance, "_style"):
instance._style = StyleArray()
coll = getattr(instance.parent.parent, self.collection)
if isinstance(value, NamedStyle):
style = value
if style not in coll:
instance.parent.parent.add_named_style(style)
elif value not in coll.names:
if value in styles: # is it builtin?
style = styles[value]
if style not in coll:
instance.parent.parent.add_named_style(style)
else:
raise ValueError("{0} is not a known style".format(value))
else:
style = coll[value]
instance._style = copy(style.as_tuple())
def __get__(self, instance, cls):
if not getattr(instance, "_style"):
instance._style = StyleArray()
idx = getattr(instance._style, self.key)
coll = getattr(instance.parent.parent, self.collection)
return coll.names[idx]
class StyleArrayDescriptor:
def __init__(self, key):
self.key = key
def __set__(self, instance, value):
if instance._style is None:
instance._style = StyleArray()
setattr(instance._style, self.key, value)
def __get__(self, instance, cls):
if instance._style is None:
return False
return bool(getattr(instance._style, self.key))
class StyleableObject:
"""
Base class for styleble objects implementing proxy and lookup functions
"""
font = StyleDescriptor('_fonts', "fontId")
fill = StyleDescriptor('_fills', "fillId")
border = StyleDescriptor('_borders', "borderId")
number_format = NumberFormatDescriptor()
protection = StyleDescriptor('_protections', "protectionId")
alignment = StyleDescriptor('_alignments', "alignmentId")
style = NamedStyleDescriptor()
quotePrefix = StyleArrayDescriptor('quotePrefix')
pivotButton = StyleArrayDescriptor('pivotButton')
__slots__ = ('parent', '_style')
def __init__(self, sheet, style_array=None):
self.parent = sheet
if style_array is not None:
style_array = StyleArray(style_array)
self._style = style_array
@property
def style_id(self):
if self._style is None:
self._style = StyleArray()
return self.parent.parent._cell_styles.add(self._style)
@property
def has_style(self):
if self._style is None:
return False
return any(self._style)

View File

@ -0,0 +1,274 @@
# 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()

View File

@ -0,0 +1,94 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors import (
Typed,
Float,
Bool,
Set,
Integer,
NoneSet,
String,
Sequence
)
from .colors import Color
class TableStyleElement(Serialisable):
tagname = "tableStyleElement"
type = Set(values=(['wholeTable', 'headerRow', 'totalRow', 'firstColumn',
'lastColumn', 'firstRowStripe', 'secondRowStripe', 'firstColumnStripe',
'secondColumnStripe', 'firstHeaderCell', 'lastHeaderCell',
'firstTotalCell', 'lastTotalCell', 'firstSubtotalColumn',
'secondSubtotalColumn', 'thirdSubtotalColumn', 'firstSubtotalRow',
'secondSubtotalRow', 'thirdSubtotalRow', 'blankRow',
'firstColumnSubheading', 'secondColumnSubheading',
'thirdColumnSubheading', 'firstRowSubheading', 'secondRowSubheading',
'thirdRowSubheading', 'pageFieldLabels', 'pageFieldValues']))
size = Integer(allow_none=True)
dxfId = Integer(allow_none=True)
def __init__(self,
type=None,
size=None,
dxfId=None,
):
self.type = type
self.size = size
self.dxfId = dxfId
class TableStyle(Serialisable):
tagname = "tableStyle"
name = String()
pivot = Bool(allow_none=True)
table = Bool(allow_none=True)
count = Integer(allow_none=True)
tableStyleElement = Sequence(expected_type=TableStyleElement, allow_none=True)
__elements__ = ('tableStyleElement',)
def __init__(self,
name=None,
pivot=None,
table=None,
count=None,
tableStyleElement=(),
):
self.name = name
self.pivot = pivot
self.table = table
self.count = count
self.tableStyleElement = tableStyleElement
class TableStyleList(Serialisable):
tagname = "tableStyles"
defaultTableStyle = String(allow_none=True)
defaultPivotStyle = String(allow_none=True)
tableStyle = Sequence(expected_type=TableStyle, allow_none=True)
__elements__ = ('tableStyle',)
__attrs__ = ("count", "defaultTableStyle", "defaultPivotStyle")
def __init__(self,
count=None,
defaultTableStyle="TableStyleMedium9",
defaultPivotStyle="PivotStyleLight16",
tableStyle=(),
):
self.defaultTableStyle = defaultTableStyle
self.defaultPivotStyle = defaultPivotStyle
self.tableStyle = tableStyle
@property
def count(self):
return len(self.tableStyle)