Source code for fastpyxl.styles.fills


# Copyright (c) 2010-2024 fastpyxl

from fastpyxl.compat import safe_string
from fastpyxl.typed_serialisable.base import Serialisable
from fastpyxl.typed_serialisable.errors import FieldValidationError
from fastpyxl.typed_serialisable.fields import AliasField, Field

from .colors import Color

from fastpyxl.xml.functions import Element, localname


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_NONE,
    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,
)


def _color_converter(value, field_name: str):
    if value is None:
        return None
    if isinstance(value, Color):
        return value
    if isinstance(value, str):
        return Color(rgb=value)
    raise FieldValidationError(f"{field_name} rejected value {value!r}")


def _enum_converter(value, allowed_values, field_name: str):
    if value is None:
        return None
    if value not in allowed_values:
        raise FieldValidationError(f"{field_name} rejected value {value!r}")
    return value


def _range_converter(value, *, field_name: str, min_value: float, max_value: float):
    if value is None:
        return None
    try:
        numeric = float(value)
    except Exception as exc:  # pragma: no cover
        raise FieldValidationError(f"{field_name} rejected value {value!r}") from exc
    if numeric < min_value or numeric > max_value:
        raise FieldValidationError(f"{field_name} rejected value {value!r}")
    return numeric


[docs] class Fill(Serialisable): """Base class""" tagname = "fill"
[docs] @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)
[docs] 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" patternType: str | None = Field.attribute( expected_type=str, allow_none=True, converter=lambda v: None if v == "none" else _enum_converter(v, fills, "patternType"), default=None, ) fill_type: str | None = AliasField("patternType", default=None) fgColor: Color | None = Field.element( expected_type=Color, allow_none=True, converter=lambda v: _color_converter(v, "fgColor"), default=None, ) start_color: Color | None = AliasField("fgColor", default=None) bgColor: Color | None = Field.element( expected_type=Color, allow_none=True, converter=lambda v: _color_converter(v, "bgColor"), default=None, ) end_color: Color | None = AliasField("bgColor", default=None) 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)
[docs] 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')
[docs] class Stop(Serialisable): tagname = "stop" position: float | None = Field.attribute( expected_type=float, allow_none=True, converter=lambda v: _range_converter(v, field_name="position", min_value=0, max_value=1), default=None, ) color: Color | None = Field.element( expected_type=Color, allow_none=True, converter=lambda v: _color_converter(v, "color"), default=None, ) def __init__(self, color, position): if position is None: raise TypeError("position cannot be None") 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
[docs] 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: str | None = Field.attribute( expected_type=str, allow_none=True, converter=lambda v: _enum_converter(v, ("linear", "path"), "type"), default=None, ) fill_type: str | None = AliasField("type", default=None) degree: float | None = Field.attribute(expected_type=float, allow_none=True, default=None) left: float | None = Field.attribute(expected_type=float, allow_none=True, default=None) right: float | None = Field.attribute(expected_type=float, allow_none=True, default=None) top: float | None = Field.attribute(expected_type=float, allow_none=True, default=None) bottom: float | None = Field.attribute(expected_type=float, allow_none=True, default=None) stop: list[Stop] = Field.sequence(expected_type=Stop, default=list) 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 = list(stop) self.type = type def __setattr__(self, name, value): if name == "stop" and value is not None: value = _assign_position(value) super().__setattr__(name, value) def __iter__(self): for attr in self.__attrs__: value = getattr(self, attr) if value: yield attr, safe_string(value)
[docs] def to_tree(self, tagname=None, namespace=None, idx=None): parent = Element("fill") el = super().to_tree() parent.append(el) return parent