|
9 | 9 | Any,
|
10 | 10 | Callable,
|
11 | 11 | Iterable,
|
| 12 | + Literal, |
12 | 13 | NamedTuple,
|
13 | 14 | Sequence,
|
14 | 15 | SupportsFloat,
|
|
34 | 35 | from pydantic_core import CoreSchema
|
35 | 36 | from typing_extensions import TypeAlias
|
36 | 37 |
|
| 38 | + rgba = Literal["r", "g", "b", "a"] |
| 39 | + |
37 | 40 | # not used internally... but available for typing
|
38 | 41 | RGBTuple: TypeAlias = "tuple[int, int, int] | tuple[float, float, float]"
|
39 | 42 | RGBATuple: TypeAlias = (
|
@@ -291,8 +294,100 @@ def _norm_name(name: str) -> str:
|
291 | 294 | return delim.sub("", name).lower()
|
292 | 295 |
|
293 | 296 |
|
| 297 | +def _ensure_format(format: str) -> Sequence[rgba]: |
| 298 | + _format = "".join(format).lower() |
| 299 | + if not all(c in "rgba" for c in _format): |
| 300 | + raise ValueError("Format must be composed of 'r', 'g', 'b', and 'a'") |
| 301 | + return _format # type: ignore [return-value] |
| 302 | + |
| 303 | + |
| 304 | +def parse_int( |
| 305 | + value: int, |
| 306 | + format: str, |
| 307 | + bits_per_component: int | Sequence[int] = 8, |
| 308 | +) -> RGBA: |
| 309 | + """Parse color from bit-shifted integer encoding. |
| 310 | +
|
| 311 | + Parameters |
| 312 | + ---------- |
| 313 | + value : int |
| 314 | + The integer value to parse. |
| 315 | + format : str |
| 316 | + The format of the integer value. Must be a string composed only of |
| 317 | + the characters 'r', 'g', 'b', and 'a'. |
| 318 | + bits_per_component : int | Sequence[int] | None |
| 319 | + The number of bits used to represent each color component. If a single |
| 320 | + integer is provided, it is used for all components. If a sequence of |
| 321 | + integers is provided, the length must match the length of `format`. |
| 322 | + """ |
| 323 | + fmt = _ensure_format(format) |
| 324 | + if isinstance(bits_per_component, int): |
| 325 | + bits_per_component = [bits_per_component] * len(fmt) |
| 326 | + elif len(bits_per_component) != len(fmt): # pragma: no cover |
| 327 | + raise ValueError("Length of 'bits_per_component' must match 'format'") |
| 328 | + |
| 329 | + components: dict[str, float] = {"r": 0, "g": 0, "b": 0, "a": 1} |
| 330 | + shift = 0 |
| 331 | + |
| 332 | + # Calculate the starting shift amount |
| 333 | + for bits in reversed(bits_per_component): |
| 334 | + shift += bits |
| 335 | + |
| 336 | + # Parse each component from the integer value |
| 337 | + for i, comp in enumerate(fmt): |
| 338 | + shift -= bits_per_component[i] |
| 339 | + mask = (1 << bits_per_component[i]) - 1 |
| 340 | + components[comp] = ((value >> shift) & mask) / mask |
| 341 | + |
| 342 | + return RGBA(**components) |
| 343 | + |
| 344 | + |
| 345 | +def to_int( |
| 346 | + color: RGBA, |
| 347 | + format: str, |
| 348 | + bits_per_component: int | Sequence[int] = 8, |
| 349 | +) -> int: |
| 350 | + """Convert color to bit-shifted integer encoding. |
| 351 | +
|
| 352 | + Parameters |
| 353 | + ---------- |
| 354 | + color : RGBA |
| 355 | + The color to convert. |
| 356 | + format : str |
| 357 | + The format of the integer value. Must be a string composed only of |
| 358 | + the characters 'r', 'g', 'b', and 'a'. |
| 359 | + bits_per_component : int | Sequence[int] | None |
| 360 | + The number of bits used to represent each color component. If a single |
| 361 | + integer is provided, it is used for all components. If a sequence of |
| 362 | + integers is provided, the length must match the length of `format`. |
| 363 | + """ |
| 364 | + fmt = _ensure_format(format) |
| 365 | + if isinstance(bits_per_component, int): |
| 366 | + bits_per_component = [bits_per_component] * len(fmt) |
| 367 | + elif len(bits_per_component) != len(fmt): # pragma: no cover |
| 368 | + raise ValueError("Length of 'bits_per_component' must match 'format'") |
| 369 | + |
| 370 | + value = 0 |
| 371 | + shift = 0 |
| 372 | + |
| 373 | + # Calculate the starting shift amount |
| 374 | + for bits in reversed(bits_per_component): |
| 375 | + shift += bits |
| 376 | + |
| 377 | + # Parse each component from the integer value |
| 378 | + for i, comp in enumerate(fmt): |
| 379 | + shift -= bits_per_component[i] |
| 380 | + mask = (1 << bits_per_component[i]) - 1 |
| 381 | + value |= int(getattr(color, comp) * mask) << shift |
| 382 | + |
| 383 | + return value |
| 384 | + |
| 385 | + |
294 | 386 | def parse_rgba(value: Any) -> RGBA:
|
295 | 387 | """Parse a color."""
|
| 388 | + if isinstance(value, RGBA): |
| 389 | + return value |
| 390 | + |
296 | 391 | # parse hex, rgb, rgba, hsl, hsla, and color name strings
|
297 | 392 | if isinstance(value, str):
|
298 | 393 | key = _norm_name(value)
|
@@ -337,11 +432,8 @@ def parse_rgba(value: Any) -> RGBA:
|
337 | 432 | return value._rgba
|
338 | 433 |
|
339 | 434 | if isinstance(value, int):
|
340 |
| - # convert 24-bit integer to RGBA8 with bit shifting |
341 |
| - r = (value >> 16) & 0xFF |
342 |
| - g = (value >> 8) & 0xFF |
343 |
| - b = value & 0xFF |
344 |
| - return RGBA8(r, g, b).to_float() |
| 435 | + # assume RGB24, use parse_int to explicitly pass format and bits_per_component |
| 436 | + return parse_int(value, "rgb") |
345 | 437 |
|
346 | 438 | # support for pydantic.color.Color
|
347 | 439 | for mod in ("pydantic", "pydantic_extra_types"):
|
@@ -388,6 +480,49 @@ def __new__(cls, value: Any) -> Color:
|
388 | 480 | _COLOR_CACHE[rgba] = obj
|
389 | 481 | return _COLOR_CACHE[rgba]
|
390 | 482 |
|
| 483 | + @classmethod |
| 484 | + def from_int( |
| 485 | + cls, |
| 486 | + value: int, |
| 487 | + format: str, |
| 488 | + bits_per_component: int | Sequence[int] = 8, |
| 489 | + ) -> Color: |
| 490 | + """Parse color from bit-shifted integer encoding. |
| 491 | +
|
| 492 | + Parameters |
| 493 | + ---------- |
| 494 | + value : int |
| 495 | + The integer value to parse. |
| 496 | + format : str |
| 497 | + The format of the integer value. Must be a string composed only of |
| 498 | + the characters 'r', 'g', 'b', and 'a'. |
| 499 | + bits_per_component : int | Sequence[int] | None |
| 500 | + The number of bits used to represent each color component. If a single |
| 501 | + integer is provided, it is used for all components. If a sequence of |
| 502 | + integers is provided, the length must match the length of `format`. |
| 503 | + """ |
| 504 | + rgba = parse_int(value, format=format, bits_per_component=bits_per_component) |
| 505 | + return cls(rgba) |
| 506 | + |
| 507 | + def to_int( |
| 508 | + self, |
| 509 | + format: str, |
| 510 | + bits_per_component: int | Sequence[int] = 8, |
| 511 | + ) -> int: |
| 512 | + """Convert color to bit-shifted integer encoding. |
| 513 | +
|
| 514 | + Parameters |
| 515 | + ---------- |
| 516 | + format : str |
| 517 | + The format of the integer value. Must be a string composed only of |
| 518 | + the characters 'r', 'g', 'b', and 'a'. |
| 519 | + bits_per_component : int | Sequence[int] | None |
| 520 | + The number of bits used to represent each color component. If a single |
| 521 | + integer is provided, it is used for all components. If a sequence of |
| 522 | + integers is provided, the length must match the length of `format`. |
| 523 | + """ |
| 524 | + return to_int(self._rgba, format=format, bits_per_component=bits_per_component) |
| 525 | + |
391 | 526 | # for mkdocstrings
|
392 | 527 | def __init__(self, value: ColorLike) -> None:
|
393 | 528 | pass
|
|
0 commit comments