1"""
2Normalization utilities for color values.
3
4"""
5
6# SPDX-License-Identifier: BSD-3-Clause
7
8from ._definitions import _HEX_COLOR_RE
9from ._types import IntegerRGB, IntTuple, PercentRGB, PercentTuple
10
11
12def normalize_hex(hex_value: str) -> str:
13 """
14 Normalize a hexadecimal color value to a string consisting of the character `#`
15 followed by six lowercase hexadecimal digits (what HTML5 terms a "valid lowercase
16 simple color").
17
18 If the supplied value cannot be interpreted as a hexadecimal color value,
19 :exc:`ValueError` is raised. See :ref:`the conventions used by this module
20 <conventions>` for information on acceptable formats for hexadecimal values.
21
22 Examples:
23
24 .. doctest::
25
26 >>> normalize_hex("#0099cc")
27 '#0099cc'
28 >>> normalize_hex("#0099CC")
29 '#0099cc'
30 >>> normalize_hex("#09c")
31 '#0099cc'
32 >>> normalize_hex("#09C")
33 '#0099cc'
34 >>> normalize_hex("#0099gg")
35 Traceback (most recent call last):
36 ...
37 ValueError: '#0099gg' is not a valid hexadecimal color value.
38 >>> normalize_hex("0099cc")
39 Traceback (most recent call last):
40 ...
41 ValueError: '0099cc' is not a valid hexadecimal color value.
42
43 :param hex_value: The hexadecimal color value to normalize.
44 :raises ValueError: when the input is not a valid hexadecimal color value.
45
46 """
47 if (match := _HEX_COLOR_RE.match(hex_value)) is None:
48 raise ValueError(f'"{hex_value}" is not a valid hexadecimal color value.')
49 hex_digits = match.group(1)
50 if len(hex_digits) == 3:
51 hex_digits = "".join(2 * s for s in hex_digits)
52 return f"#{hex_digits.lower()}"
53
54
55def _normalize_integer_rgb(value: int) -> int:
56 """
57 Internal normalization function for clipping integer values into the permitted
58 range (0-255, inclusive).
59
60 """
61 return 0 if value < 0 else 255 if value > 255 else value
62
63
64def normalize_integer_triplet(rgb_triplet: IntTuple) -> IntegerRGB:
65 """
66 Normalize an integer ``rgb()`` triplet so that all values are within the range
67 0..255.
68
69 Examples:
70
71 .. doctest::
72
73 >>> normalize_integer_triplet((128, 128, 128))
74 IntegerRGB(red=128, green=128, blue=128)
75 >>> normalize_integer_triplet((0, 0, 0))
76 IntegerRGB(red=0, green=0, blue=0)
77 >>> normalize_integer_triplet((255, 255, 255))
78 IntegerRGB(red=255, green=255, blue=255)
79 >>> normalize_integer_triplet((270, -20, -0))
80 IntegerRGB(red=255, green=0, blue=0)
81
82 :param rgb_triplet: The percentage `rgb()` triplet to normalize.
83
84 """
85 return IntegerRGB._make(_normalize_integer_rgb(value) for value in rgb_triplet)
86
87
88def _normalize_percent_rgb(value: str) -> str:
89 """
90 Internal normalization function for clipping percent values into the permitted
91 range (0%-100%, inclusive).
92
93 """
94 value = value.split("%")[0]
95 percent = float(value) if "." in value else int(value)
96
97 return "0%" if percent < 0 else "100%" if percent > 100 else f"{percent}%"
98
99
100def normalize_percent_triplet(rgb_triplet: PercentTuple) -> PercentRGB:
101 """
102 Normalize a percentage ``rgb()`` triplet so that all values are within the range
103 0%..100%.
104
105 Examples:
106
107 .. doctest::
108
109 >>> normalize_percent_triplet(("50%", "50%", "50%"))
110 PercentRGB(red='50%', green='50%', blue='50%')
111 >>> normalize_percent_triplet(("0%", "100%", "0%"))
112 PercentRGB(red='0%', green='100%', blue='0%')
113 >>> normalize_percent_triplet(("-10%", "-0%", "500%"))
114 PercentRGB(red='0%', green='0%', blue='100%')
115
116 :param rgb_triplet: The percentage `rgb()` triplet to normalize.
117
118 """
119 return PercentRGB._make(_normalize_percent_rgb(value) for value in rgb_triplet)
120
121
122def _percent_to_integer(percent: str) -> int:
123 """
124 Internal helper for converting a percentage value to an integer between 0 and
125 255 inclusive.
126
127 """
128 return int(round(float(percent.split("%")[0]) / 100 * 255))