|
1 | 1 | """Color types for tppt.""" |
2 | 2 |
|
3 | | -from typing import NamedTuple, TypeAlias, assert_never, overload |
| 3 | +from dataclasses import dataclass |
| 4 | +from typing import TypeAlias, assert_never, overload |
4 | 5 |
|
5 | | -from tppt.exception import ColorInvalidFormatError |
| 6 | +from pptx.dml.color import RGBColor as _PptxRGBColor |
| 7 | +from typing_extensions import Annotated, Doc |
6 | 8 |
|
7 | | -LiteralColor: TypeAlias = tuple[int, int, int] | str |
| 9 | +from tppt.exception import ( |
| 10 | + ColorInvalidFormatError, |
| 11 | + ColorInvalidTupleSizeError, |
| 12 | + InvalidColorValueError, |
| 13 | +) |
8 | 14 |
|
| 15 | +LiteralRGBColor: TypeAlias = tuple[int, int, int] |
| 16 | +LiteralRGBAColor: TypeAlias = tuple[int, int, int, int] |
| 17 | +LiteralColor: TypeAlias = LiteralRGBColor | LiteralRGBAColor | str |
9 | 18 |
|
10 | | -class RGBColor(NamedTuple): |
11 | | - r: int |
12 | | - g: int |
13 | | - b: int |
| 19 | + |
| 20 | +@dataclass |
| 21 | +class Color: |
| 22 | + r: Annotated[int, Doc("red color value")] |
| 23 | + g: Annotated[int, Doc("green color value")] |
| 24 | + b: Annotated[int, Doc("blue color value")] |
| 25 | + a: Annotated[int | None, Doc("alpha color value")] = None |
| 26 | + |
| 27 | + def __post_init__(self): |
| 28 | + if not 0 <= self.r <= 255: |
| 29 | + raise InvalidColorValueError("red", self.r) |
| 30 | + if not 0 <= self.g <= 255: |
| 31 | + raise InvalidColorValueError("green", self.g) |
| 32 | + if not 0 <= self.b <= 255: |
| 33 | + raise InvalidColorValueError("blue", self.b) |
| 34 | + if self.a is not None and not 0 <= self.a <= 255: |
| 35 | + raise InvalidColorValueError("alpha", self.a) |
14 | 36 |
|
15 | 37 |
|
16 | 38 | @overload |
17 | | -def to_rgb_color(color: RGBColor | LiteralColor) -> RGBColor: ... |
| 39 | +def to_rgb_color(color: Color | LiteralColor | _PptxRGBColor) -> Color: ... |
18 | 40 |
|
19 | 41 |
|
20 | 42 | @overload |
21 | | -def to_rgb_color(color: RGBColor | LiteralColor | None) -> RGBColor | None: ... |
| 43 | +def to_rgb_color( |
| 44 | + color: Color | LiteralColor | _PptxRGBColor | None, |
| 45 | +) -> Color | None: ... |
22 | 46 |
|
23 | 47 |
|
24 | | -def to_rgb_color(color: RGBColor | LiteralColor | None) -> RGBColor | None: |
| 48 | +def to_rgb_color(color: Color | LiteralColor | _PptxRGBColor | None) -> Color | None: |
25 | 49 | match color: |
26 | 50 | case None: |
27 | 51 | return None |
28 | | - case tuple(): |
29 | | - return RGBColor(*color) |
30 | 52 | case str(): |
31 | 53 | if not color.startswith("#"): |
32 | 54 | raise ColorInvalidFormatError(color) |
33 | 55 |
|
34 | 56 | match len(color): |
35 | | - case 4: |
| 57 | + case 4 | 5: |
36 | 58 | # Note that #123 is the same as #112233 |
37 | 59 | r = int(color[1:2] * 2, 16) |
38 | 60 | g = int(color[2:3] * 2, 16) |
39 | 61 | b = int(color[3:4] * 2, 16) |
40 | | - return RGBColor(r, g, b) |
| 62 | + a = int(color[4:5] * 2, 16) if len(color) == 5 else None |
| 63 | + return Color(r, g, b, a) |
41 | 64 |
|
42 | | - case 7: |
| 65 | + case 7 | 9: |
43 | 66 | r = int(color[1:3], 16) |
44 | 67 | g = int(color[3:5], 16) |
45 | 68 | b = int(color[5:7], 16) |
46 | | - return RGBColor(r, g, b) |
| 69 | + a = int(color[7:9], 16) if len(color) == 9 else None |
| 70 | + return Color(r, g, b, a) |
47 | 71 |
|
48 | 72 | case _: |
49 | 73 | raise ColorInvalidFormatError(color) |
| 74 | + case tuple(): |
| 75 | + match color: |
| 76 | + case tuple() if len(color) == 3: |
| 77 | + r, g, b = color |
| 78 | + return Color(r, g, b) |
| 79 | + case tuple() if len(color) == 4: |
| 80 | + r, g, b, a = color |
| 81 | + return Color(r, g, b, a) |
| 82 | + case _: |
| 83 | + raise ColorInvalidTupleSizeError(color) |
| 84 | + case Color(): |
| 85 | + return color |
50 | 86 | case _: |
51 | 87 | assert_never(color) |
0 commit comments