1from __future__ import absolute_import, division
2# Copyright (c) 2010-2019 openpyxl
3
4from openpyxl.descriptors import (
5    Float,
6    Set,
7    Alias,
8    NoneSet,
9    Sequence,
10    Integer,
11    MinMax,
12)
13from openpyxl.descriptors.serialisable import Serialisable
14from openpyxl.compat import safe_string
15
16from .colors import ColorDescriptor, Color
17
18from openpyxl.xml.functions import Element, localname
19from openpyxl.xml.constants import SHEET_MAIN_NS
20
21
22FILL_NONE = 'none'
23FILL_SOLID = 'solid'
24FILL_PATTERN_DARKDOWN = 'darkDown'
25FILL_PATTERN_DARKGRAY = 'darkGray'
26FILL_PATTERN_DARKGRID = 'darkGrid'
27FILL_PATTERN_DARKHORIZONTAL = 'darkHorizontal'
28FILL_PATTERN_DARKTRELLIS = 'darkTrellis'
29FILL_PATTERN_DARKUP = 'darkUp'
30FILL_PATTERN_DARKVERTICAL = 'darkVertical'
31FILL_PATTERN_GRAY0625 = 'gray0625'
32FILL_PATTERN_GRAY125 = 'gray125'
33FILL_PATTERN_LIGHTDOWN = 'lightDown'
34FILL_PATTERN_LIGHTGRAY = 'lightGray'
35FILL_PATTERN_LIGHTGRID = 'lightGrid'
36FILL_PATTERN_LIGHTHORIZONTAL = 'lightHorizontal'
37FILL_PATTERN_LIGHTTRELLIS = 'lightTrellis'
38FILL_PATTERN_LIGHTUP = 'lightUp'
39FILL_PATTERN_LIGHTVERTICAL = 'lightVertical'
40FILL_PATTERN_MEDIUMGRAY = 'mediumGray'
41
42fills = (FILL_SOLID, FILL_PATTERN_DARKDOWN, FILL_PATTERN_DARKGRAY,
43         FILL_PATTERN_DARKGRID, FILL_PATTERN_DARKHORIZONTAL, FILL_PATTERN_DARKTRELLIS,
44         FILL_PATTERN_DARKUP, FILL_PATTERN_DARKVERTICAL, FILL_PATTERN_GRAY0625,
45         FILL_PATTERN_GRAY125, FILL_PATTERN_LIGHTDOWN, FILL_PATTERN_LIGHTGRAY,
46         FILL_PATTERN_LIGHTGRID, FILL_PATTERN_LIGHTHORIZONTAL,
47         FILL_PATTERN_LIGHTTRELLIS, FILL_PATTERN_LIGHTUP, FILL_PATTERN_LIGHTVERTICAL,
48         FILL_PATTERN_MEDIUMGRAY)
49
50
51class Fill(Serialisable):
52
53    """Base class"""
54
55    tagname = "fill"
56
57    @classmethod
58    def from_tree(cls, el):
59        children = [c for c in el]
60        if not children:
61            return
62        child = children[0]
63        if "patternFill" in child.tag:
64            return PatternFill._from_tree(child)
65        return super(Fill, GradientFill).from_tree(child)
66
67
68class PatternFill(Fill):
69    """Area fill patterns for use in styles.
70    Caution: if you do not specify a fill_type, other attributes will have
71    no effect !"""
72
73    tagname = "patternFill"
74
75    __elements__ = ('fgColor', 'bgColor')
76
77    patternType = NoneSet(values=fills)
78    fill_type = Alias("patternType")
79    fgColor = ColorDescriptor()
80    start_color = Alias("fgColor")
81    bgColor = ColorDescriptor()
82    end_color = Alias("bgColor")
83
84    def __init__(self, patternType=None, fgColor=Color(), bgColor=Color(),
85                 fill_type=None, start_color=None, end_color=None):
86        if fill_type is not None:
87            patternType = fill_type
88        self.patternType = patternType
89        if start_color is not None:
90            fgColor = start_color
91        self.fgColor = fgColor
92        if end_color is not None:
93            bgColor = end_color
94        self.bgColor = bgColor
95
96    @classmethod
97    def _from_tree(cls, el):
98        attrib = dict(el.attrib)
99        for child in el:
100            desc = localname(child)
101            attrib[desc] = Color.from_tree(child)
102        return cls(**attrib)
103
104
105    def to_tree(self, tagname=None, idx=None):
106        parent = Element("fill")
107        el = Element(self.tagname)
108        if self.patternType is not None:
109            el.set('patternType', self.patternType)
110        for c in self.__elements__:
111            value = getattr(self, c)
112            if value != Color():
113                el.append(value.to_tree(c))
114        parent.append(el)
115        return parent
116
117
118DEFAULT_EMPTY_FILL = PatternFill()
119DEFAULT_GRAY_FILL = PatternFill(patternType='gray125')
120
121
122class Stop(Serialisable):
123
124    tagname = "stop"
125
126    position = MinMax(min=0, max=1)
127    color = ColorDescriptor()
128
129    def __init__(self, color, position):
130        self.position = position
131        self.color = color
132
133
134def _assign_position(values):
135    """
136    Automatically assign positions if a list of colours is provided.
137
138    It is not permitted to mix colours and stops
139    """
140    n_values = len(values)
141    n_stops = sum(isinstance(value, Stop) for value in values)
142
143    if n_stops == 0:
144        interval = 1
145        if n_values > 2:
146            interval = 1 / (n_values - 1)
147        values = [Stop(value, i * interval)
148                  for i, value in enumerate(values)]
149
150    elif n_stops < n_values:
151        raise ValueError('Cannot interpret mix of Stops and Colors in GradientFill')
152
153    pos = set()
154    for stop in values:
155        if stop.position in pos:
156            raise ValueError("Duplicate position {0}".format(stop.position))
157        pos.add(stop.position)
158
159    return values
160
161
162class StopList(Sequence):
163
164    expected_type = Stop
165
166    def __set__(self, obj, values):
167        values = _assign_position(values)
168        super(StopList, self).__set__(obj, values)
169
170
171class GradientFill(Fill):
172    """Fill areas with gradient
173
174    Two types of gradient fill are supported:
175
176        - A type='linear' gradient interpolates colours between
177          a set of specified Stops, across the length of an area.
178          The gradient is left-to-right by default, but this
179          orientation can be modified with the degree
180          attribute.  A list of Colors can be provided instead
181          and they will be positioned with equal distance between them.
182
183        - A type='path' gradient applies a linear gradient from each
184          edge of the area. Attributes top, right, bottom, left specify
185          the extent of fill from the respective borders. Thus top="0.2"
186          will fill the top 20% of the cell.
187
188    """
189
190    tagname = "gradientFill"
191
192    type = Set(values=('linear', 'path'))
193    fill_type = Alias("type")
194    degree = Float()
195    left = Float()
196    right = Float()
197    top = Float()
198    bottom = Float()
199    stop = StopList()
200
201
202    def __init__(self, type="linear", degree=0, left=0, right=0, top=0,
203                 bottom=0, stop=()):
204        self.degree = degree
205        self.left = left
206        self.right = right
207        self.top = top
208        self.bottom = bottom
209        self.stop = stop
210        self.type = type
211
212
213    def __iter__(self):
214        for attr in self.__attrs__:
215            value = getattr(self, attr)
216            if value:
217                yield attr, safe_string(value)
218
219
220    def to_tree(self, tagname=None, namespace=None, idx=None):
221        parent = Element("fill")
222        el = super(GradientFill, self).to_tree()
223        parent.append(el)
224        return parent
225