"""Brand assets and image utilities for DRC visuals."""
from __future__ import annotations
from collections.abc import Sequence
from importlib import import_module as _import_module
from importlib.resources import files as _resource_files
from os import PathLike
from typing import Literal
import matplotlib.colors as _mpc
import numpy as _np
from PIL import Image as _Image
LogoLayout = Literal["horizontal", "stacked", "symbol"]
PatternVariant = Literal["full", "grey", "white"]
ScribbleWeight = Literal["thin", "thick"]
BLACK = "#000000"
DARK_TEAL = "#1A4C49"
TEAL = "#4D8687"
BLUE = "#57B7BA"
ORANGE = "#EA8534"
RED = "#DF5127"
#: Canonical DRC palette in display order.
COLORS = [BLACK, DARK_TEAL, TEAL, BLUE, ORANGE, RED]
#: Semantic palette map for direct lookup in plotting and style code.
BRAND_COLORS = {
"black": BLACK,
"dark_teal": DARK_TEAL,
"teal": TEAL,
"blue": BLUE,
"orange": ORANGE,
"red": RED,
}
PRIMARY_FONT_FAMILY = "Magdelin"
SECONDARY_FONT_FAMILY = "Zilla Slab"
PRIMARY_FONT_USAGE = "ALL CAPS headers"
SECONDARY_FONT_USAGE = "body text"
_DATA_DIR = _resource_files("drcutils") / "data"
_BRAND_ASSETS_DIR = _DATA_DIR / "brand_assets"
_RESAMPLING = getattr(_Image, "Resampling", _Image)
_LOGO_VARIANTS = {
"horizontal": {
"full": "full.png",
"black": "black.png",
"dark_teal": "dark_teal.png",
"red": "red.png",
"white": "white.png",
},
"stacked": {
"full": "full.png",
"black": "black.png",
"dark_teal": "dark_teal.png",
"red": "red.png",
"white": "white.png",
},
"symbol": {
"full": "full.png",
"black": "black.png",
"dark_teal": "dark_teal.png",
"blue": "blue.png",
"teal": "teal.png",
"orange": "orange.png",
"red": "red.png",
"white": "white.png",
},
}
_ON_BLACK_LOGOS = {
"horizontal": "horizontal.png",
"stacked": "stacked.png",
"symbol": "symbol.png",
}
_PATTERNS = {"full": "full.png", "grey": "grey.png", "white": "white.png"}
_CIRCLE_COLORS = {
"blue": "blue.png",
"dark_teal": "dark_teal.png",
"orange": "orange.png",
"red": "red.png",
"teal": "teal.png",
"white": "white.png",
}
#: Path to an SVG of the symbol logo.
LOGO_ONLY_SVG = str(_DATA_DIR / "logo.svg")
#: Path to a PNG of the full-color symbol logo.
LOGO_ONLY_PNG = str(_BRAND_ASSETS_DIR / "logos" / "symbol" / "full.png")
#: Path to an STL of the symbol logo.
LOGO_ONLY_STL = str(_DATA_DIR / "logo.stl")
#: Path to a PNG of the full-color horizontal logo.
HORIZONTAL_LOGO_PNG = str(_BRAND_ASSETS_DIR / "logos" / "horizontal" / "full.png")
#: Path to a PNG of the full-color stacked logo.
STACKED_LOGO_PNG = str(_BRAND_ASSETS_DIR / "logos" / "stacked" / "full.png")
#: Path to the white pattern PNG.
WHITE_PATTERN_PNG = str(_BRAND_ASSETS_DIR / "patterns" / "white.png")
#: Path to the grey pattern PNG.
GREY_PATTERN_PNG = str(_BRAND_ASSETS_DIR / "patterns" / "grey.png")
#: Path to the full-color pattern PNG.
COLOR_PATTERN_PNG = str(_BRAND_ASSETS_DIR / "patterns" / "full.png")
def _normalize_key(value: str) -> str:
return value.strip().lower().replace("-", "_").replace(" ", "_")
def _normalize_color_key(value: str) -> str:
normalized = _normalize_key(value)
if normalized == "gray":
return "grey"
return normalized
[docs]
def get_logo_path(
layout: str,
variant: str,
on_black: bool = False,
fmt: str = "png",
) -> str:
"""Resolve the canonical path for a packaged logo variant."""
layout_key = _normalize_key(layout)
if layout_key not in _LOGO_VARIANTS:
allowed_layouts = ", ".join(sorted(_LOGO_VARIANTS))
raise ValueError(f"Unsupported logo layout '{layout}'. Choose from: {allowed_layouts}.")
if fmt.lower() != "png":
raise ValueError("Only PNG logo assets are packaged.")
if on_black:
return str(_BRAND_ASSETS_DIR / "logos" / "on_black" / _ON_BLACK_LOGOS[layout_key])
variant_key = _normalize_color_key(variant)
if variant_key == "auto":
variant_key = "full"
allowed_variants = _LOGO_VARIANTS[layout_key]
if variant_key not in allowed_variants:
valid = ", ".join(sorted(allowed_variants))
raise ValueError(
f"Unsupported variant '{variant}' for layout '{layout_key}'. Choose from: {valid}."
)
return str(_BRAND_ASSETS_DIR / "logos" / layout_key / allowed_variants[variant_key])
[docs]
def get_pattern_path(variant: str) -> str:
"""Resolve the canonical path for packaged pattern imagery."""
variant_key = _normalize_color_key(variant)
if variant_key not in _PATTERNS:
valid = ", ".join(sorted(_PATTERNS))
raise ValueError(f"Unsupported pattern variant '{variant}'. Choose from: {valid}.")
return str(_BRAND_ASSETS_DIR / "patterns" / _PATTERNS[variant_key])
[docs]
def get_gradient_paths() -> list[str]:
"""Return ordered packaged gradient asset paths."""
return [str(_BRAND_ASSETS_DIR / "gradients" / f"{idx:02d}.png") for idx in range(1, 7)]
[docs]
def get_circle_graphic_path(color: str) -> str:
"""Resolve the canonical path for a packaged circle graphic."""
color_key = _normalize_color_key(color)
if color_key not in _CIRCLE_COLORS:
valid = ", ".join(sorted(_CIRCLE_COLORS))
raise ValueError(f"Unsupported circle color '{color}'. Choose from: {valid}.")
return str(_BRAND_ASSETS_DIR / "circles" / _CIRCLE_COLORS[color_key])
[docs]
def get_scribble_path(weight: str, color: str) -> str:
"""Resolve the canonical path for a packaged scribble graphic."""
weight_key = _normalize_key(weight)
if weight_key not in {"thin", "thick"}:
raise ValueError("Scribble weight must be 'thin' or 'thick'.")
color_key = _normalize_color_key(color)
if color_key not in _CIRCLE_COLORS:
valid = ", ".join(sorted(_CIRCLE_COLORS))
raise ValueError(f"Unsupported scribble color '{color}'. Choose from: {valid}.")
return str(_BRAND_ASSETS_DIR / "scribbles" / weight_key / f"{color_key}.png")
[docs]
def get_matplotlib_font_fallbacks() -> dict[str, list[str]]:
"""Return recommended Matplotlib font fallback stacks."""
return {
"primary": [PRIMARY_FONT_FAMILY, "Arial", "Helvetica", "sans-serif"],
"secondary": [SECONDARY_FONT_FAMILY, "Georgia", "Times New Roman", "serif"],
}
def _parse_flag_size(size: Sequence[object] | None) -> tuple[list[int], int]:
if size is None:
return [50, 10, 10, 10, 10, 10], 100
if not isinstance(size, (list, tuple)) or len(size) != 2:
raise ValueError(
"size must be a 2-item list/tuple: [color_widths, height] or [height, color_widths]."
)
first, second = size
if isinstance(first, int) and isinstance(second, (list, tuple)):
height = first
color_widths = second
elif isinstance(second, int) and isinstance(first, (list, tuple)):
color_widths = first
height = second
else:
raise ValueError("size must contain one int and one list/tuple of color widths.")
if not isinstance(height, int) or height <= 0:
raise ValueError("height must be a positive integer.")
parsed_widths: list[int] = []
for idx, width in enumerate(color_widths):
if not isinstance(width, int) or width <= 0:
raise ValueError(f"color width at index {idx} must be a positive integer.")
parsed_widths.append(width)
if len(parsed_widths) != len(COLORS):
raise ValueError(f"Expected {len(COLORS)} color widths; received {len(parsed_widths)}.")
return parsed_widths, height
def _parse_watermark_box(
box: Sequence[float | None] | None,
) -> tuple[float, float, float | None, float | None]:
raw = [0.0, 0.0, 0.10, None] if box is None else list(box)
if len(raw) != 4:
raise ValueError("box must be a 4-item sequence: [x, y, width_ratio, height_ratio].")
x_raw, y_raw, width_raw, height_raw = raw
if x_raw is None or y_raw is None:
raise ValueError("box x and y positions must be numeric values in [0, 1].")
x = float(x_raw)
y = float(y_raw)
if not (0.0 <= x <= 1.0) or not (0.0 <= y <= 1.0):
raise ValueError("box x and y must be in [0, 1].")
width_ratio: float | None = None
height_ratio: float | None = None
if width_raw is not None:
width_ratio = float(width_raw)
if not (0.0 < width_ratio <= 1.0):
raise ValueError("box width_ratio must be in (0, 1].")
if height_raw is not None:
height_ratio = float(height_raw)
if not (0.0 < height_ratio <= 1.0):
raise ValueError("box height_ratio must be in (0, 1].")
return x, y, width_ratio, height_ratio
def _resize_logo(
source_size: tuple[int, int],
logo_image: _Image.Image,
width_ratio: float | None,
height_ratio: float | None,
) -> _Image.Image:
source_width, source_height = source_size
logo_width, logo_height = logo_image.size
if width_ratio is None and height_ratio is None:
return logo_image.copy()
if width_ratio is None and height_ratio is not None:
target_height = max(1, int(round(source_height * height_ratio)))
target_width = max(1, int(round(logo_width * target_height / logo_height)))
return logo_image.resize((target_width, target_height), _RESAMPLING.LANCZOS)
if width_ratio is not None and height_ratio is None:
target_width = max(1, int(round(source_width * width_ratio)))
target_height = max(1, int(round(logo_height * target_width / logo_width)))
return logo_image.resize((target_width, target_height), _RESAMPLING.LANCZOS)
target_width = max(1, int(round(source_width * float(width_ratio))))
target_height = max(1, int(round(source_height * float(height_ratio))))
return logo_image.resize((target_width, target_height), _RESAMPLING.LANCZOS)
def _is_dark_region(
source_rgba: _Image.Image,
x_position: int,
y_position: int,
width: int,
height: int,
) -> bool:
x0 = max(0, x_position)
y0 = max(0, y_position)
x1 = min(source_rgba.size[0], x_position + width)
y1 = min(source_rgba.size[1], y_position + height)
if x1 <= x0 or y1 <= y0:
return False
region = source_rgba.crop((x0, y0, x1, y1)).convert("RGB")
region_array = _np.asarray(region, dtype=float)
if region_array.size == 0:
return False
luminance = (
0.2126 * region_array[..., 0]
+ 0.7152 * region_array[..., 1]
+ 0.0722 * region_array[..., 2]
)
return float(luminance.mean()) < 110.0
[docs]
def flag(
output_filepath: str | bytes | PathLike | None = None,
size: Sequence[object] | None = None,
):
"""Create a DRC color flag image."""
color_widths, height = _parse_flag_size(size)
rgb_colors = [_np.array(_mpc.to_rgb(c)) for c in COLORS]
color_row: list[_np.ndarray] = []
for idx, color_width in enumerate(color_widths):
color_row.extend([rgb_colors[idx]] * color_width)
pixel_data = _np.uint8(_np.array([color_row] * height) * 255)
flag_image = _Image.fromarray(pixel_data, mode="RGB")
if output_filepath is None:
return flag_image
flag_image.save(output_filepath)
return None
[docs]
def watermark(
filepath: str | bytes | PathLike,
output_filepath: str | bytes | PathLike | None = None,
watermark_filepath: str | bytes | PathLike | None = None,
box: Sequence[float | None] | None = None,
*,
logo_layout: str = "stacked",
logo_variant: str = "auto",
on_black: bool | Literal["auto"] = "auto",
) -> None:
"""Watermark an image using packaged DRC assets or a custom watermark file."""
x, y, width_ratio, height_ratio = _parse_watermark_box(box)
source_image = _Image.open(filepath)
source_rgba = source_image.convert("RGBA")
x_position = int(round(source_rgba.size[0] * x))
y_position = int(round(source_rgba.size[1] * y))
if watermark_filepath is not None:
logo_image = _Image.open(watermark_filepath).convert("RGBA")
else:
variant_key = _normalize_color_key(logo_variant)
if variant_key == "auto":
variant_key = "full"
if on_black == "auto":
probe_logo = _Image.open(
get_logo_path(logo_layout, variant_key, on_black=False)
).convert("RGBA")
probe_resized = _resize_logo(source_rgba.size, probe_logo, width_ratio, height_ratio)
use_on_black = _is_dark_region(
source_rgba,
x_position,
y_position,
probe_resized.size[0],
probe_resized.size[1],
)
elif isinstance(on_black, bool):
use_on_black = on_black
else:
raise ValueError("on_black must be True, False, or 'auto'.")
logo_path = get_logo_path(logo_layout, variant_key, on_black=use_on_black)
logo_image = _Image.open(logo_path).convert("RGBA")
resized_logo = _resize_logo(source_rgba.size, logo_image, width_ratio, height_ratio)
overlay = _Image.new("RGBA", source_rgba.size, (0, 0, 0, 0))
overlay.paste(resized_logo, (x_position, y_position), resized_logo)
composited = _Image.alpha_composite(source_rgba, overlay)
if source_image.mode == "RGBA":
output_image = composited
else:
output_image = composited.convert(source_image.mode)
target_path = filepath if output_filepath is None else output_filepath
output_image.save(target_path)
def __getattr__(name: str) -> object:
if name == "colormaps":
return _import_module(".colormaps", __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"BLACK",
"BLUE",
"BRAND_COLORS",
"COLORS",
"COLOR_PATTERN_PNG",
"DARK_TEAL",
"GREY_PATTERN_PNG",
"HORIZONTAL_LOGO_PNG",
"LOGO_ONLY_PNG",
"LOGO_ONLY_STL",
"LOGO_ONLY_SVG",
"LogoLayout",
"ORANGE",
"PatternVariant",
"PRIMARY_FONT_FAMILY",
"PRIMARY_FONT_USAGE",
"RED",
"SECONDARY_FONT_FAMILY",
"SECONDARY_FONT_USAGE",
"STACKED_LOGO_PNG",
"ScribbleWeight",
"TEAL",
"WHITE_PATTERN_PNG",
"colormaps",
"flag",
"get_circle_graphic_path",
"get_gradient_paths",
"get_logo_path",
"get_matplotlib_font_fallbacks",
"get_pattern_path",
"get_scribble_path",
"watermark",
]