Skip to content

docx_plus.styles.theme

Read-only theme color and font resolution. The cascade resolver in styles.inspect uses these to translate themeColor attributes into concrete hex values and *Theme font tokens (minorHAnsi, …) into typeface names. Writing themes is out of scope — this module is read-only (SPEC §1).

docx_plus.styles.theme

Read-only theme color resolution.

WordprocessingML references theme colors symbolically (themeColor="accent1") with optional themeTint/themeShade modifiers. The actual RGB values live in word/theme/theme1.xml under a:clrScheme. This module reads that scheme, translates Word's ST_ThemeColor names to DrawingML scheme keys (ECMA-376 17.18.97), and applies the tint/shade/lumMod/lumOff transforms defined in ECMA-376 17.18.40.

Failures here are recoverable: a missing or malformed theme part is reported by :func:load_theme returning None (or a partially-populated scheme), not by raising. Callers — primarily the cascade resolver — fold that into a partial=True flag on the resolved formatting. SPEC §4 "Theme references".

The same scheme also exposes the theme's fonts (a:fontScheme): :func:resolve_theme_font maps a WordprocessingML font-theme token (w:asciiTheme="minorHAnsi" etc.) to the concrete typeface the theme defines ("Calibri"), so the cascade can report a real font name rather than the bare token.

The w:color cascade element (ECMA-376 CT_Color) carries only themeTint / themeShade, so :func:resolve_theme_color applies just those two transforms. :func:apply_lum_mod / :func:apply_lum_off implement the DrawingML lumMod / lumOff transforms for callers that read theme colors referenced from DrawingML (shape fills, w14 text effects), where those transforms do appear — they are deliberately not part of the w:color resolution path because that element cannot carry them.

The module is read-only; writing themes is a v0.2 non-goal (SPEC §1).

ThemeColors dataclass

ThemeColors(scheme: dict[str, str], fonts: dict[str, str] = dict())

Resolved theme color + font scheme.

Built by :func:load_theme. Use :meth:base to look up a color by Word's ST_ThemeColor name and :meth:font to look up a typeface by ST_Theme font token (both are what appear in WordprocessingML).

Attributes:

Name Type Description
scheme dict[str, str]

DrawingML color key ("accent1", "dk1", …) -> uppercase RRGGBB hex.

fonts dict[str, str]

ST_Theme font token ("minorHAnsi", "majorEastAsia", …) -> concrete typeface name. Empty when the theme has no a:fontScheme.

base

base(theme_name: str) -> str | None

Return the unmodified hex color for a Word theme color name.

Parameters:

Name Type Description Default
theme_name str

A value from ST_ThemeColor (e.g. "accent1", "text1").

required

Returns:

Type Description
str | None

Uppercase RRGGBB hex string, or None if the name is not a

str | None

recognized theme color or the underlying scheme entry is missing.

Source code in docx_plus/styles/theme.py
def base(self, theme_name: str) -> str | None:
    """Return the unmodified hex color for a Word theme color name.

    Args:
        theme_name: A value from ``ST_ThemeColor`` (e.g. ``"accent1"``,
            ``"text1"``).

    Returns:
        Uppercase ``RRGGBB`` hex string, or ``None`` if the name is not a
        recognized theme color or the underlying scheme entry is missing.
    """
    key = _THEME_NAME_TO_SCHEME_KEY.get(theme_name)
    if key is None:
        return None
    return self.scheme.get(key)

font

font(token: str) -> str | None

Return the concrete typeface for an ST_Theme font token.

Parameters:

Name Type Description Default
token str

A w:asciiTheme / w:hAnsiTheme / w:eastAsiaTheme / w:cstheme value such as "minorHAnsi" or "majorEastAsia".

required

Returns:

Type Description
str | None

The typeface name from the theme's a:fontScheme (e.g.

str | None

"Calibri"), or None if the token is unknown or the

str | None

scheme entry is empty / missing.

Source code in docx_plus/styles/theme.py
def font(self, token: str) -> str | None:
    """Return the concrete typeface for an ``ST_Theme`` font token.

    Args:
        token: A ``w:asciiTheme`` / ``w:hAnsiTheme`` /
            ``w:eastAsiaTheme`` / ``w:cstheme`` value such as
            ``"minorHAnsi"`` or ``"majorEastAsia"``.

    Returns:
        The typeface name from the theme's ``a:fontScheme`` (e.g.
        ``"Calibri"``), or ``None`` if the token is unknown or the
        scheme entry is empty / missing.
    """
    return self.fonts.get(token)

ThemeError

Bases: DocxPlusError

Raised when theme inputs are structurally invalid in an unrecoverable way.

Most theme defects (missing part, malformed XML, unknown name) are reported via None returns or partial=True per SPEC §4. This error is reserved for programmer-error cases such as an unparseable hex transform byte that would otherwise pass through silently.

load_theme

load_theme(doc: Document) -> ThemeColors | None

Read word/theme/theme1.xml and return its color scheme.

Parameters:

Name Type Description Default
doc Document

A python-docx :class:~docx.document.Document.

required

Returns:

Name Type Description
A ThemeColors | None

class:ThemeColors describing the document's color scheme, or

ThemeColors | None

None if the document has no theme part attached or the theme part

ThemeColors | None

cannot be parsed at all. A partially-readable scheme is returned as a

ThemeColors | None

ThemeColors whose .scheme dict simply omits the unreadable

ThemeColors | None

entries — callers can detect partiality via :meth:ThemeColors.base

ThemeColors | None

returning None.

Source code in docx_plus/styles/theme.py
def load_theme(doc: Document) -> ThemeColors | None:
    """Read ``word/theme/theme1.xml`` and return its color scheme.

    Args:
        doc: A python-docx :class:`~docx.document.Document`.

    Returns:
        A :class:`ThemeColors` describing the document's color scheme, or
        ``None`` if the document has no theme part attached or the theme part
        cannot be parsed at all. A partially-readable scheme is returned as a
        ``ThemeColors`` whose ``.scheme`` dict simply omits the unreadable
        entries — callers can detect partiality via :meth:`ThemeColors.base`
        returning ``None``.
    """
    theme_xml = _read_theme_blob(doc)
    if theme_xml is None:
        return None
    try:
        root = etree.fromstring(theme_xml)
    except etree.XMLSyntaxError:
        return None
    return ThemeColors(scheme=_parse_clr_scheme(root), fonts=_parse_font_scheme(root))

resolve_theme_color

resolve_theme_color(
    theme: ThemeColors | None,
    name: str,
    *,
    tint: str | None = None,
    shade: str | None = None,
) -> str | None

Resolve a WordprocessingML theme color reference to RRGGBB hex.

Parameters:

Name Type Description Default
theme ThemeColors | None

The document's theme scheme, or None if no theme part is attached. None always resolves to None.

required
name str

Word ST_ThemeColor value (e.g. "accent1", "text1", "none").

required
tint str | None

Optional w:themeTint value — a hex byte "00"-"FF". Lightens the resolved color toward white.

None
shade str | None

Optional w:themeShade value — a hex byte "00"-"FF". Darkens the resolved color toward black.

None

Returns:

Type Description
str | None

Uppercase RRGGBB hex string, or None if the name is unknown,

str | None

the theme is absent, or the name is the literal "none".

Note

WordprocessingML treats themeTint and themeShade as mutually exclusive in practice, but this function tolerates both being set: the shade is applied first, then the tint, matching the order Word uses when it encounters the (unusual) combination.

Source code in docx_plus/styles/theme.py
def resolve_theme_color(
    theme: ThemeColors | None,
    name: str,
    *,
    tint: str | None = None,
    shade: str | None = None,
) -> str | None:
    """Resolve a WordprocessingML theme color reference to ``RRGGBB`` hex.

    Args:
        theme: The document's theme scheme, or ``None`` if no theme part is
            attached. ``None`` always resolves to ``None``.
        name: Word ``ST_ThemeColor`` value (e.g. ``"accent1"``, ``"text1"``,
            ``"none"``).
        tint: Optional ``w:themeTint`` value — a hex byte ``"00"``-``"FF"``.
            Lightens the resolved color toward white.
        shade: Optional ``w:themeShade`` value — a hex byte ``"00"``-``"FF"``.
            Darkens the resolved color toward black.

    Returns:
        Uppercase ``RRGGBB`` hex string, or ``None`` if the name is unknown,
        the theme is absent, or the name is the literal ``"none"``.

    Note:
        WordprocessingML treats ``themeTint`` and ``themeShade`` as mutually
        exclusive in practice, but this function tolerates both being set: the
        shade is applied first, then the tint, matching the order Word uses
        when it encounters the (unusual) combination.
    """
    if theme is None or name == "none":
        return None
    base = theme.base(name)
    if base is None:
        return None
    out = base
    if shade is not None:
        out = apply_theme_shade(out, shade)
    if tint is not None:
        out = apply_theme_tint(out, tint)
    return out

resolve_theme_font

resolve_theme_font(theme: ThemeColors | None, token: str) -> str | None

Resolve a WordprocessingML font-theme token to a concrete typeface.

Parameters:

Name Type Description Default
theme ThemeColors | None

The document's theme, or None if no theme part is attached. None always resolves to None.

required
token str

An ST_Theme value (e.g. "minorHAnsi", "majorEastAsia").

required

Returns:

Type Description
str | None

The typeface name (e.g. "Calibri"), or None if the theme is

str | None

absent or the token has no entry in the font scheme.

Source code in docx_plus/styles/theme.py
def resolve_theme_font(theme: ThemeColors | None, token: str) -> str | None:
    """Resolve a WordprocessingML font-theme token to a concrete typeface.

    Args:
        theme: The document's theme, or ``None`` if no theme part is
            attached. ``None`` always resolves to ``None``.
        token: An ``ST_Theme`` value (e.g. ``"minorHAnsi"``,
            ``"majorEastAsia"``).

    Returns:
        The typeface name (e.g. ``"Calibri"``), or ``None`` if the theme is
        absent or the token has no entry in the font scheme.
    """
    if theme is None:
        return None
    return theme.font(token)

apply_theme_tint

apply_theme_tint(hex_color: str, tint_byte: str) -> str

Lighten hex_color toward white by ECMA-376 17.18.40 themeTint.

Algorithm: convert to HSL, replace L with L * t + (1 - t) where t = int(tint_byte, 16) / 255. tint="FF" is a no-op; tint="00" forces L to 1 (pure white).

Parameters:

Name Type Description Default
hex_color str

Six-character hex color (with or without leading #).

required
tint_byte str

Hex byte "00"-"FF".

required

Returns:

Type Description
str

Uppercase RRGGBB hex string.

Source code in docx_plus/styles/theme.py
def apply_theme_tint(hex_color: str, tint_byte: str) -> str:
    """Lighten ``hex_color`` toward white by ECMA-376 17.18.40 ``themeTint``.

    Algorithm: convert to HSL, replace ``L`` with ``L * t + (1 - t)`` where
    ``t = int(tint_byte, 16) / 255``. ``tint="FF"`` is a no-op; ``tint="00"``
    forces L to 1 (pure white).

    Args:
        hex_color: Six-character hex color (with or without leading ``#``).
        tint_byte: Hex byte ``"00"``-``"FF"``.

    Returns:
        Uppercase ``RRGGBB`` hex string.
    """
    t = _parse_hex_byte(tint_byte)
    h, lum, s = _rgb_to_hls(hex_color)
    new_l = lum * t + (1 - t)
    return _hls_to_hex(h, new_l, s)

apply_theme_shade

apply_theme_shade(hex_color: str, shade_byte: str) -> str

Darken hex_color toward black by ECMA-376 17.18.40 themeShade.

Algorithm: convert to HSL, replace L with L * s where s = int(shade_byte, 16) / 255. shade="FF" is a no-op; shade="00" forces L to 0 (pure black).

Parameters:

Name Type Description Default
hex_color str

Six-character hex color (with or without leading #).

required
shade_byte str

Hex byte "00"-"FF".

required

Returns:

Type Description
str

Uppercase RRGGBB hex string.

Source code in docx_plus/styles/theme.py
def apply_theme_shade(hex_color: str, shade_byte: str) -> str:
    """Darken ``hex_color`` toward black by ECMA-376 17.18.40 ``themeShade``.

    Algorithm: convert to HSL, replace ``L`` with ``L * s`` where
    ``s = int(shade_byte, 16) / 255``. ``shade="FF"`` is a no-op; ``shade="00"``
    forces L to 0 (pure black).

    Args:
        hex_color: Six-character hex color (with or without leading ``#``).
        shade_byte: Hex byte ``"00"``-``"FF"``.

    Returns:
        Uppercase ``RRGGBB`` hex string.
    """
    s_val = _parse_hex_byte(shade_byte)
    h, lum, sat = _rgb_to_hls(hex_color)
    return _hls_to_hex(h, lum * s_val, sat)

apply_lum_mod

apply_lum_mod(hex_color: str, lum_mod: int) -> str

Multiply L by lum_mod / 100000 per ECMA-376 17.18.40.

DrawingML transform values are percent thousandths: 50000 means 50%.

Parameters:

Name Type Description Default
hex_color str

Six-character hex color.

required
lum_mod int

Percent thousandths (e.g. 50000 for 50%).

required

Returns:

Type Description
str

Uppercase RRGGBB hex string with L clamped to [0, 1].

Source code in docx_plus/styles/theme.py
def apply_lum_mod(hex_color: str, lum_mod: int) -> str:
    """Multiply L by ``lum_mod / 100000`` per ECMA-376 17.18.40.

    DrawingML transform values are percent thousandths: ``50000`` means 50%.

    Args:
        hex_color: Six-character hex color.
        lum_mod: Percent thousandths (e.g. ``50000`` for 50%).

    Returns:
        Uppercase ``RRGGBB`` hex string with L clamped to ``[0, 1]``.
    """
    factor = lum_mod / 100000.0
    h, lum, sat = _rgb_to_hls(hex_color)
    return _hls_to_hex(h, lum * factor, sat)

apply_lum_off

apply_lum_off(hex_color: str, lum_off: int) -> str

Add lum_off / 100000 to L per ECMA-376 17.18.40.

DrawingML transform values are percent thousandths: 80000 adds 0.8. The result is clamped to [0, 1].

Parameters:

Name Type Description Default
hex_color str

Six-character hex color.

required
lum_off int

Percent thousandths (e.g. 80000 for +0.8).

required

Returns:

Type Description
str

Uppercase RRGGBB hex string.

Source code in docx_plus/styles/theme.py
def apply_lum_off(hex_color: str, lum_off: int) -> str:
    """Add ``lum_off / 100000`` to L per ECMA-376 17.18.40.

    DrawingML transform values are percent thousandths: ``80000`` adds 0.8.
    The result is clamped to ``[0, 1]``.

    Args:
        hex_color: Six-character hex color.
        lum_off: Percent thousandths (e.g. ``80000`` for +0.8).

    Returns:
        Uppercase ``RRGGBB`` hex string.
    """
    delta = lum_off / 100000.0
    h, lum, sat = _rgb_to_hls(hex_color)
    return _hls_to_hex(h, lum + delta, sat)