import abc from enum import Enum import openpyxl.styles from openpyxl.cell import Cell from openpyxl.styles import Side, Border, Font, PatternFill, Alignment from openpyxl.styles.numbers import FORMAT_GENERAL, FORMAT_NUMBER_00 # Formatting Enums class SCO_COLORS(Enum): def __new__(cls, value, argb): obj = object.__new__(cls) obj._value_ = value obj.argb = argb return obj NONE = (0, None) BLACK = (1, "FF000000") WHITE = (2, "FFFFFFFF") RED = (3, "FFFF0000") BROWN = (4, "FF993300") PURPLE = (5, "FF993366") BLUE = (6, "FF0000FF") ORANGE = (7, "FFFF3300") LIGHT_YELLOW = (8, "FFFFFF99") GREEN = (9, "FF00FF00") GREY = (10, "FF101010") BUT1 = (23, "FF95B3D7") RCUE1 = (24, "FFB8CCE4") UE1 = (25, "FFDCE6F1") BUT2 = (26, "FFC4D79B") RCUE2 = (27, "FFD8E4BC") UE2 = (28, "FFEBF1DE") BUT3 = (29, "FFFABF8F") RCUE3 = (30, "FFFCD5B4") UE3 = (31, "FFFDE9D9") class SCO_BORDERTHICKNESS(Enum): def __new__(cls, value, width): obj = object.__new__(cls) obj._value_ = value obj.width = width return obj NONE = (0, None) BORDER_HAIR = (1, "hair") BORDER_THIN = (2, "thin") BORDER_MEDIUM = (3, "medium") BORDER_THICK = (4, "thick") class SCO_FONTNAME(Enum): def __new__(cls, value, fontname): obj = object.__new__(cls) obj._value_ = value obj.fontname = fontname return obj NONE = (0, None) FONTNAME_CALIBRI = (1, "Calibri") FONTNAME_ARIAL = (2, "Arial") FONTNAME_COURIER = (3, "Courier New") FONTNAME_TIMES = (4, "Times New Roman") class SCO_FONTSIZE(Enum): def __new__(cls, value, fontsize): obj = object.__new__(cls) obj._value_ = value obj.fontsize = fontsize return obj NONE = (0, None) FONTSIZE_9 = (1, 9.0) FONTSIZE_10 = (2, 10.0) FONTSIZE_11 = (2, 11.0) FONTSIZE_13 = (4, 13.0) class SCO_NUMBER_FORMAT(Enum): def __new__(cls, value, format): obj = object.__new__(cls) obj._value_ = value obj.format = format return obj NONE = (0, None) NUMBER_GENERAL = (0, FORMAT_GENERAL) NUMBER_00 = (1, FORMAT_NUMBER_00) NUMBER_0 = (2, "0.0") class SCO_HALIGN(Enum): def __new__(cls, value, position): obj = object.__new__(cls) obj._value_ = value obj.position = position return obj NONE = (0, None) HALIGN_LEFT = (1, "left") HALIGN_CENTER = (2, "center") HALIGN_RIGHT = (3, "right") class SCO_VALIGN(Enum): def __new__(cls, value, position): obj = object.__new__(cls) obj._value_ = value obj.position = position return obj VALIGN_BOTTOM = (0, "bottom") VALIGN_TOP = (1, "top") VALIGN_CENTER = (2, "center") # Composante (bitfield) atomique. Based on Enums free = 0 class Composante(abc.ABC): def __init__(self, base=None, width: int = 1): global free if base is None: self.base = free free += width else: self.base = base self.width = width self.end = self.base + self.width self.mask = ((1 << width) - 1) << self.base def read(self, signature: int) -> int: return (signature & self.mask) >> self.base def clear(self, signature: int) -> int: return signature & ~self.mask def write(self, index, signature=0) -> int: return self.clear(signature) + (index << self.base) def make_zero_based_constant(self, enums: list[Enum] = None): return 0 @abc.abstractmethod def build(self, value: int): pass class Composante_boolean(Composante): def __init__(self): super().__init__(width=1) def set(self, data: bool, signature=0) -> int: return self.write(1 if data else 0, signature) def build(self, signature) -> bool: value = self.read(signature) assert value < (1 << self.width) return value == 1 class Composante_number_format(Composante): WIDTH: int = 2 def __init__(self): assert (1 << self.WIDTH) > SCO_NUMBER_FORMAT.__len__() super().__init__(width=self.WIDTH) def set(self, data: SCO_NUMBER_FORMAT, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_NUMBER_FORMAT: value = self.read(signature) assert value < (1 << self.width) return SCO_NUMBER_FORMAT(value) class Composante_Colors(Composante): WIDTH: int = 5 def __init__(self, default: SCO_COLORS = SCO_COLORS.BLACK): assert (1 << self.WIDTH) > SCO_COLORS.__len__() super().__init__(width=self.WIDTH) self.default: SCO_COLORS = default def set(self, data: SCO_COLORS, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_COLORS: value = self.read(signature) assert value < (1 << self.width) if value == 0: return None try: return SCO_COLORS(value) except: return self.default class Composante_borderThickness(Composante): WIDTH: int = 3 def __init__(self): assert (1 << self.WIDTH) > SCO_BORDERTHICKNESS.__len__() super().__init__(width=self.WIDTH) def set(self, data: SCO_BORDERTHICKNESS, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_BORDERTHICKNESS: value = self.read(signature) assert value < (1 << self.width) try: return SCO_BORDERTHICKNESS(value) except: return None class Composante_fontname(Composante): WIDTH: int = 3 def __init__(self): assert (1 << self.WIDTH) > SCO_FONTNAME.__len__() super().__init__(width=self.WIDTH) def set(self, data: SCO_FONTNAME, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_FONTNAME: value = self.read(signature) assert value < (1 << self.width) try: return SCO_FONTNAME(value) except: return SCO_FONTNAME.FONTNAME_CALIBRI class Composante_fontsize(Composante): WIDTH: int = 3 def __init__(self): assert (1 << self.WIDTH) > SCO_FONTSIZE.__len__() super().__init__(width=self.WIDTH) def set(self, data: SCO_FONTSIZE, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_FONTSIZE: value = self.read(signature) assert value < (1 << self.width) return SCO_FONTSIZE(value) or None class Composante_halign(Composante): WIDTH: int = 3 def __init__(self): assert (1 << self.WIDTH) > SCO_HALIGN.__len__() super().__init__(width=self.WIDTH) def set(self, data: SCO_HALIGN, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_HALIGN: value = self.read(signature) assert value < (1 << self.width) try: return SCO_HALIGN(value) except: return SCO_HALIGN.HALIGN_LEFT class Composante_valign(Composante): WIDTH: int = 3 def __init__(self): assert (1 << self.WIDTH) > SCO_VALIGN.__len__() super().__init__(width=self.WIDTH) def set(self, data: SCO_VALIGN, signature=0) -> int: return self.write(data.value, signature) def build(self, signature: int) -> SCO_VALIGN: value = self.read(signature) assert value < (1 << self.width) try: return SCO_VALIGN(value) except: return SCO_VALIGN.VALIGN_CENTER # Formatting objects class Sco_Fill: def __init__(self, color: SCO_COLORS): self.color = color def to_openpyxl(self): return PatternFill( fill_type="solid", fgColor=None if self.color is None else self.color.argb, ) class Sco_BorderSide: def __init__( self, thickness: SCO_BORDERTHICKNESS = None, color: SCO_COLORS = SCO_COLORS.WHITE, ): self.thickness = thickness self.color: SCO_COLORS = color def to_openpyxl(self): return Side( border_style=None if self.thickness is None else self.thickness.width, color=None if self.color is None else self.color.argb, ) class Sco_Borders: def __init__( self, left: Sco_BorderSide = None, right: Sco_BorderSide = None, top: Sco_BorderSide = None, bottom: Sco_BorderSide = None, ): self.left = left self.right = right self.top = top self.bottom = bottom def to_openpyxl(self): return Border( left=self.left.to_openpyxl(), right=self.right.to_openpyxl(), top=self.top.to_openpyxl(), bottom=self.bottom.to_openpyxl(), ) class Sco_Alignment: def __init__( self, halign: SCO_HALIGN = None, valign: SCO_VALIGN = None, ): self.halign = halign self.valign = valign def to_openpyxl(self): return Alignment( horizontal=None if self.halign is None else self.halign.position, vertical=None if self.valign is None else self.valign.position, ) class Sco_Font: def __init__( self, name: SCO_FONTNAME = SCO_FONTNAME(0), fontsize: SCO_FONTSIZE = SCO_FONTSIZE(0), bold: bool = None, italic: bool = None, outline: bool = None, color: "SCO_COLORS" = None, ): self.name = name self.bold = bold self.italic = italic self.outline = outline self.color = color self.fontsize = fontsize def to_openpyxl(self): return Font( name=None if self.name is None else self.name.fontname, size=None if self.fontsize is None else self.fontsize.fontsize, bold=self.bold, italic=self.italic, outline=self.outline, color=None if self.color is None else self.color.argb, ) class Sco_Style: from openpyxl.cell import Cell def __init__( self, font: Sco_Font = None, fill: Sco_Fill = None, alignment: Sco_Alignment = None, borders: Sco_Borders = None, number_format: SCO_NUMBER_FORMAT = FORMAT_GENERAL, ): self.font = font or None self.fill = fill or None self.alignment = alignment or None self.borders = borders or None self.number_format = number_format or SCO_NUMBER_FORMAT.NUMBER_GENERAL def apply(self, cell: Cell): if self.font: cell.font = self.font.to_openpyxl() if self.fill and self.fill.color: cell.fill = self.fill.to_openpyxl() if self.alignment: cell.alignment = self.alignment.to_openpyxl() if self.borders: cell.border = self.borders.to_openpyxl() cell.number_format = ( FORMAT_GENERAL if self.number_format is None or self.number_format == SCO_NUMBER_FORMAT.NONE else self.number_format.format ) # Composantes groupant d'autres composantes et dotées d'un mécanisme de cache class Composante_group(Composante): def __init__(self, composantes: list[Composante]): self.composantes = composantes self.cache = {} mini = min([comp.base for comp in composantes]) maxi = max([comp.end for comp in composantes]) width = sum([comp.width for comp in composantes]) if not width == (maxi - mini): raise Exception("Composante group non complete ou non connexe") super().__init__(base=mini, width=width) def lookup_or_cache(self, signature: int): value = self.read(signature) assert value < (1 << self.width) if not value in self.cache: self.cache[value] = self.build(signature) return self.cache[value] def make_zero_based_constant(self, enums: list[Enum] = None) -> int: if enums is None: return 0 signature = 0 for enum, composante in zip(enums, self.composantes): signature += composante.write(enum.value) return signature >> self.base class Composante_fill(Composante_group): def __init__(self, color: Composante_Colors): super().__init__([color]) self.color = color def build(self, signature: int) -> Sco_Fill: return Sco_Fill(color=self.color.build(signature)) class Composante_font(Composante_group): def __init__( self, name: Composante_fontname, fontsize: Composante_fontsize, color: Composante_Colors, bold: Composante_boolean, italic: Composante_boolean, outline: Composante_boolean, ): super().__init__([name, fontsize, color, bold, italic, outline]) self.name = name self.fontsize = fontsize self.color = color self.bold = bold self.italic = italic self.outline = outline def build(self, signature: int) -> Sco_Font: return Sco_Font( name=self.name.build(signature), fontsize=self.fontsize.build(signature), color=self.color.build(signature), bold=self.bold.build(signature), italic=self.italic.build(signature), outline=self.outline.build(signature), ) class Composante_border(Composante_group): def __init__(self, thick: Composante_borderThickness, color: Composante_Colors): super().__init__([thick, color]) self.thick = thick self.color = color def build(self, signature: int) -> Sco_BorderSide: return Sco_BorderSide( thickness=self.thick.build(signature), color=self.color.build(signature), ) class Composante_borders(Composante_group): def __init__( self, left: Composante_border, right: Composante_border, top: Composante_border, bottom: Composante_border, ): super().__init__([left, right, top, bottom]) self.left = left self.right = right self.top = top self.bottom = bottom def build(self, signature: int) -> Sco_Borders: return Sco_Borders( left=self.left.lookup_or_cache(signature), right=self.right.lookup_or_cache(signature), top=self.top.lookup_or_cache(signature), bottom=self.bottom.lookup_or_cache(signature), ) class Composante_alignment(Composante_group): def __init__(self, halign: Composante_halign, valign: Composante_valign): super().__init__([halign, valign]) self.halign = halign self.valign = valign def build(self, signature: int) -> Sco_Alignment: return Sco_Alignment( halign=self.halign.build(signature), valign=self.valign.build(signature), ) class Composante_all(Composante_group): def __init__( self, font: Composante_font, fill: Composante_fill, borders: Composante_borders, alignment: Composante_alignment, number_format: Composante_number_format, ): super().__init__([font, fill, borders, alignment, number_format]) assert self.width < 64 self.font = font self.fill = fill self.borders = borders self.alignment = alignment self.number_format = number_format def build(self, signature: int) -> Sco_Style: return Sco_Style( fill=self.fill.lookup_or_cache(signature), font=self.font.lookup_or_cache(signature), borders=self.borders.lookup_or_cache(signature), alignment=self.alignment.lookup_or_cache(signature), number_format=self.number_format.build(signature), ) def get_style(self, signature: int): return self.lookup_or_cache(signature) class FMT(Enum): def __init__(self, composante: Composante): self.composante = composante def write(self, value, signature=0) -> int: return self.composante.write(value, signature) def set(self, data, signature: int = 0) -> int: return self.composante.set(data, signature) def get_style(self, signature: int): return self.composante.lookup_or_cache(signature) def make_zero_based_constant(self, enums: list[Enum]) -> int: return self.composante.make_zero_based_constant(enums=enums) def apply(self, cell: Cell, signature: int): self.composante.build(signature).apply(cell) @classmethod def compose(cls, composition: list[("FMT", int)], signature: int = 0) -> int: for field, value in composition: signature = field.write(value, field.composante.clear(signature)) return signature @classmethod def style(cls, signature: int = None) -> Sco_Style: return FMT.ALL.get_style(signature) FONT_NAME = Composante_fontname() FONT_SIZE = Composante_fontsize() FONT_COLOR = Composante_Colors() FONT_BOLD = Composante_boolean() FONT_ITALIC = Composante_boolean() FONT_OUTLINE = Composante_boolean() BORDER_LEFT_STYLE = Composante_borderThickness() BORDER_LEFT_COLOR = Composante_Colors() BORDER_RIGHT_STYLE = Composante_borderThickness() BORDER_RIGHT_COLOR = Composante_Colors() BORDER_TOP_STYLE = Composante_borderThickness() BORDER_TOP_COLOR = Composante_Colors() BORDER_BOTTOM_STYLE = Composante_borderThickness() BORDER_BOTTOM_COLOR = Composante_Colors() FILL_BGCOLOR = Composante_Colors(None) ALIGNMENT_HALIGN = Composante_halign() ALIGNMENT_VALIGN = Composante_valign() NUMBER_FORMAT = Composante_number_format() FONT = Composante_font( FONT_NAME, FONT_SIZE, FONT_COLOR, FONT_BOLD, FONT_ITALIC, FONT_OUTLINE ) FILL = Composante_fill(FILL_BGCOLOR) BORDER_LEFT = Composante_border(BORDER_LEFT_STYLE, BORDER_LEFT_COLOR) BORDER_RIGHT = Composante_border(BORDER_RIGHT_STYLE, BORDER_RIGHT_COLOR) BORDER_TOP = Composante_border(BORDER_TOP_STYLE, BORDER_TOP_COLOR) BORDER_BOTTOM = Composante_border(BORDER_BOTTOM_STYLE, BORDER_BOTTOM_COLOR) BORDERS = Composante_borders(BORDER_LEFT, BORDER_RIGHT, BORDER_TOP, BORDER_BOTTOM) ALIGNMENT = Composante_alignment(ALIGNMENT_HALIGN, ALIGNMENT_VALIGN) ALL = Composante_all(FONT, FILL, BORDERS, ALIGNMENT, NUMBER_FORMAT) fmt_atomics = { FMT.FONT_NAME, FMT.FONT_SIZE, FMT.FONT_COLOR, FMT.FONT_BOLD, FMT.FONT_ITALIC, FMT.FONT_OUTLINE, FMT.BORDER_LEFT_STYLE, FMT.BORDER_LEFT_COLOR, FMT.BORDER_RIGHT_STYLE, FMT.BORDER_RIGHT_COLOR, FMT.BORDER_TOP_STYLE, FMT.BORDER_TOP_COLOR, FMT.BORDER_BOTTOM_STYLE, FMT.BORDER_BOTTOM_COLOR, FMT.FILL_BGCOLOR, FMT.ALIGNMENT_HALIGN, FMT.ALIGNMENT_VALIGN, FMT.NUMBER_FORMAT, } HAIR_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( enums=[SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK] ) THIN_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( enums=[SCO_BORDERTHICKNESS.BORDER_THIN, SCO_COLORS.BLACK] ) MEDIUM_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( enums=[SCO_BORDERTHICKNESS.BORDER_MEDIUM, SCO_COLORS.BLACK] ) THICK_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( enums=[SCO_BORDERTHICKNESS.BORDER_THICK, SCO_COLORS.BLACK] )