dicebear-core 10.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dicebear/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """DiceBear core — deterministic, customizable, vector-based avatars.
2
+
3
+ A faithful port of ``@dicebear/core`` (JS) and ``dicebear/core`` (PHP) that
4
+ produces byte-identical SVG output for the same style and options.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .avatar import Avatar
10
+ from .errors import (
11
+ CircularColorReferenceError,
12
+ OptionsValidationError,
13
+ StyleValidationError,
14
+ ValidationError,
15
+ )
16
+ from .options_descriptor import OptionsDescriptor
17
+ from .style import Style
18
+ from .utils.color import Color
19
+
20
+ # Public surface mirrors the JS index (Avatar, Style, Color, OptionsDescriptor)
21
+ # plus the exception types Python consumers catch. Internals — Options, Prng,
22
+ # Resolver, Renderer — remain importable from their submodules but are not
23
+ # re-exported here.
24
+ __all__ = [
25
+ "Avatar",
26
+ "CircularColorReferenceError",
27
+ "Color",
28
+ "OptionsDescriptor",
29
+ "OptionsValidationError",
30
+ "Style",
31
+ "StyleValidationError",
32
+ "ValidationError",
33
+ ]
dicebear/avatar.py ADDED
@@ -0,0 +1,52 @@
1
+ """Top-level entry point for rendering an avatar from a style and options."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from typing import Any
7
+ from urllib.parse import quote
8
+
9
+ from .options import Options
10
+ from .renderer import Renderer
11
+ from .resolver import Resolver
12
+ from .style import Style
13
+
14
+ # encodeURIComponent leaves A-Za-z0-9 and -_.!~*'() unescaped. Python's quote
15
+ # always keeps letters, digits, and _.-~; adding !*'() to ``safe`` reproduces
16
+ # the JS set exactly (and drops the default '/' so it is escaped like JS does).
17
+ _DATA_URI_SAFE = "!*'()"
18
+
19
+
20
+ class Avatar:
21
+ """Top-level entry point for rendering an avatar from a style and options.
22
+
23
+ Construction immediately resolves and renders the SVG; the various accessor
24
+ methods return different serializations of that result.
25
+ """
26
+
27
+ def __init__(
28
+ self, style_input: Any, options_input: dict[str, Any] | None = None
29
+ ) -> None:
30
+ style = style_input if isinstance(style_input, Style) else Style(style_input)
31
+ options = Options(options_input)
32
+ resolver = Resolver(style, options)
33
+
34
+ self._svg = Renderer(style, resolver).render()
35
+ self._resolved_options = resolver.resolved()
36
+
37
+ def __str__(self) -> str:
38
+ return self._svg
39
+
40
+ def to_string(self) -> str:
41
+ """Return the rendered SVG markup."""
42
+ return self._svg
43
+
44
+ def to_json(self) -> dict[str, Any]:
45
+ """Return ``{"svg", "options"}`` — the SVG and the resolved options."""
46
+ return {"svg": self._svg, "options": copy.deepcopy(self._resolved_options)}
47
+
48
+ def to_data_uri(self) -> str:
49
+ """Return the SVG encoded as a ``data:image/svg+xml`` URI."""
50
+ return "data:image/svg+xml;charset=utf-8," + quote(
51
+ self._svg, safe=_DATA_URI_SAFE
52
+ )
dicebear/errors.py ADDED
@@ -0,0 +1,66 @@
1
+ """Domain error types raised by the core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypedDict
6
+
7
+
8
+ class ErrorDetail(TypedDict, total=False):
9
+ """A single schema-validation failure."""
10
+
11
+ message: str
12
+ instancePath: str
13
+
14
+
15
+ class ValidationError(RuntimeError):
16
+ """Base class for schema validation errors.
17
+
18
+ Carries the prefix in the exception message and the per-field failures in
19
+ :attr:`details`.
20
+ """
21
+
22
+ def __init__(self, prefix: str, details: list[ErrorDetail]) -> None:
23
+ parts: list[str] = []
24
+
25
+ for detail in details:
26
+ segments: list[str] = []
27
+
28
+ instance_path = detail.get("instancePath", "")
29
+ if instance_path != "":
30
+ segments.append(instance_path)
31
+
32
+ message = detail.get("message", "")
33
+ if message != "":
34
+ segments.append(message)
35
+
36
+ parts.append(" ".join(segments))
37
+
38
+ super().__init__(prefix + ": " + ", ".join(parts))
39
+ self.details = details
40
+
41
+
42
+ class OptionsValidationError(ValidationError):
43
+ """Raised when avatar options fail schema validation."""
44
+
45
+ def __init__(self, details: list[ErrorDetail]) -> None:
46
+ super().__init__("Invalid options", details)
47
+
48
+
49
+ class StyleValidationError(ValidationError):
50
+ """Raised when a style definition fails schema validation."""
51
+
52
+ def __init__(self, details: list[ErrorDetail]) -> None:
53
+ super().__init__("Invalid style definition", details)
54
+
55
+
56
+ class CircularColorReferenceError(RuntimeError):
57
+ """Raised when a color references itself, directly or indirectly.
58
+
59
+ The :attr:`chain` field reproduces the resolution path.
60
+ """
61
+
62
+ def __init__(self, chain: list[str]) -> None:
63
+ path = " → ".join(chain)
64
+
65
+ super().__init__(f"Circular color reference: {path}")
66
+ self.chain = chain
dicebear/options.py ADDED
@@ -0,0 +1,138 @@
1
+ """User-supplied option parsing and normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from typing import Any, cast
7
+
8
+ from .validator import OptionsValidator
9
+
10
+ Numeric = int | float
11
+ Range = dict[str, Numeric]
12
+
13
+
14
+ class Options:
15
+ """Validates raw user options and exposes them through typed accessors.
16
+
17
+ Each accessor returns the user's input in a normalized form (always a list
18
+ for options that accept either a scalar or a list, or ``None`` when the
19
+ option is not set), so consumers — chiefly :class:`Resolver` — never have to
20
+ do their own normalization.
21
+
22
+ Resolution against the style definition and the PRNG happens in
23
+ :class:`Resolver`; this class is purely about reading user input.
24
+ """
25
+
26
+ def __init__(self, data: dict[str, Any] | None = None) -> None:
27
+ data = data if data is not None else {}
28
+ OptionsValidator.validate(data)
29
+
30
+ # Deep-copy so later caller mutations cannot leak into resolution,
31
+ # mirroring the JS core's structuredClone and Style's deep copy.
32
+ self._data = copy.deepcopy(data)
33
+
34
+ def seed(self) -> str | None:
35
+ return cast("str | None", self._data.get("seed"))
36
+
37
+ def size(self) -> int | None:
38
+ return cast("int | None", self._data.get("size"))
39
+
40
+ def id_randomization(self) -> bool | None:
41
+ return cast("bool | None", self._data.get("idRandomization"))
42
+
43
+ def title(self) -> str | None:
44
+ return cast("str | None", self._data.get("title"))
45
+
46
+ def flip(self) -> list[str]:
47
+ return self._as_array(self._data.get("flip"))
48
+
49
+ def font_family(self) -> list[str]:
50
+ return self._as_array(self._data.get("fontFamily"))
51
+
52
+ def font_weight(self) -> list[Numeric]:
53
+ return self._as_array(self._data.get("fontWeight"))
54
+
55
+ def scale(self) -> Range | None:
56
+ return self._to_range(self._data.get("scale"))
57
+
58
+ def border_radius(self) -> Range | None:
59
+ return self._to_range(self._data.get("borderRadius"))
60
+
61
+ def rotate(self) -> Range | None:
62
+ return self._to_range(self._data.get("rotate"))
63
+
64
+ def translate_x(self) -> Range | None:
65
+ return self._to_range(self._data.get("translateX"))
66
+
67
+ def translate_y(self) -> Range | None:
68
+ return self._to_range(self._data.get("translateY"))
69
+
70
+ def component_variant(self, name: str) -> dict[str, Numeric] | None:
71
+ """Return the variant constraint for ``name`` as a weighted map, or
72
+ ``None`` when ``{name}Variant`` is unset.
73
+
74
+ A bare string or string list is normalized to a map weighted ``1`` each.
75
+ """
76
+ raw = self._data.get(name + "Variant")
77
+
78
+ if raw is None:
79
+ return None
80
+
81
+ if isinstance(raw, str):
82
+ return {raw: 1}
83
+
84
+ if isinstance(raw, list):
85
+ return dict.fromkeys(raw, 1)
86
+
87
+ return cast("dict[str, Numeric]", raw)
88
+
89
+ def component_probability(self, name: str) -> Numeric | None:
90
+ return cast("Numeric | None", self._data.get(name + "Probability"))
91
+
92
+ def color(self, name: str) -> list[str] | None:
93
+ """Return ``None`` (not ``[]``) when ``{name}Color`` is unset so the
94
+ resolver can fall back to the style definition's color values.
95
+ """
96
+ raw = self._data.get(name + "Color")
97
+
98
+ return None if raw is None else self._as_array(raw)
99
+
100
+ def color_fill(self, name: str) -> list[str]:
101
+ return self._as_array(self._data.get(name + "ColorFill"))
102
+
103
+ def color_angle(self, name: str) -> Range | None:
104
+ return self._to_range(self._data.get(name + "ColorAngle"))
105
+
106
+ def color_fill_stops(self, name: str) -> Range | None:
107
+ return self._to_range(self._data.get(name + "ColorFillStops"))
108
+
109
+ @staticmethod
110
+ def _as_array(value: Any) -> list[Any]:
111
+ if value is None:
112
+ return []
113
+
114
+ return value if isinstance(value, list) else [value]
115
+
116
+ @staticmethod
117
+ def _to_range(value: Any) -> Range | None:
118
+ """Normalize a range option (bare number, ``[n]``, ``[min, max]``, or
119
+ ``None``).
120
+
121
+ A bare number ``n`` — or a single-element array ``[n]`` — becomes
122
+ ``{'min': n, 'max': n}`` (a fixed value). An array's smaller/larger
123
+ element is taken as min/max. An empty array is treated as unset
124
+ (``None``), so the resolver applies the option's default.
125
+ """
126
+ if value is None:
127
+ return None
128
+
129
+ if isinstance(value, bool):
130
+ return None
131
+
132
+ if isinstance(value, (int, float)):
133
+ return {"min": value, "max": value}
134
+
135
+ if isinstance(value, list) and len(value) > 0:
136
+ return {"min": min(value), "max": max(value)}
137
+
138
+ return None
@@ -0,0 +1,90 @@
1
+ """Builds a descriptor of every option a given style accepts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from typing import Any
7
+
8
+ from .style import Style
9
+
10
+ _ROTATE_RANGE: dict[str, Any] = {"type": "range", "min": -360, "max": 360}
11
+ _TRANSLATE_RANGE: dict[str, Any] = {"type": "range", "min": -1000, "max": 1000}
12
+
13
+
14
+ class OptionsDescriptor:
15
+ """Builds a descriptor of every option a given style accepts.
16
+
17
+ Tooling such as the editor uses the result to render form controls and
18
+ validation hints without having to introspect the style itself.
19
+ """
20
+
21
+ def __init__(self, style: Style) -> None:
22
+ self._style = style
23
+ self._descriptor: dict[str, Any] | None = None
24
+
25
+ def to_json(self) -> dict[str, Any]:
26
+ """Return the descriptor, building it lazily on first call.
27
+
28
+ Each call returns an independent deep copy so callers can mutate the
29
+ result without affecting the cached descriptor.
30
+ """
31
+ if self._descriptor is None:
32
+ self._descriptor = self._build()
33
+
34
+ return copy.deepcopy(self._descriptor)
35
+
36
+ def _build(self) -> dict[str, Any]:
37
+ """Walk the style's components and colors and assemble the field map."""
38
+ result: dict[str, Any] = {
39
+ "seed": {"type": "string"},
40
+ "size": {"type": "number", "min": 1, "max": 4096},
41
+ "idRandomization": {"type": "boolean"},
42
+ "title": {"type": "string"},
43
+ "flip": {
44
+ "type": "enum",
45
+ "values": ["none", "horizontal", "vertical", "both"],
46
+ "list": True,
47
+ },
48
+ "fontFamily": {"type": "string", "list": True},
49
+ "fontWeight": {"type": "number", "min": 1, "max": 1000, "list": True},
50
+ "scale": {"type": "range", "min": 0, "max": 10},
51
+ "borderRadius": {"type": "range", "min": 0, "max": 50},
52
+ "rotate": dict(_ROTATE_RANGE),
53
+ "translateX": dict(_TRANSLATE_RANGE),
54
+ "translateY": dict(_TRANSLATE_RANGE),
55
+ }
56
+
57
+ for name, component in self._style.components().items():
58
+ if component.extends_name() is not None:
59
+ continue
60
+
61
+ variants = sorted(component.variants().keys())
62
+
63
+ result[f"{name}Variant"] = {
64
+ "type": "enum",
65
+ "values": variants,
66
+ "list": True,
67
+ "weighted": True,
68
+ }
69
+ result[f"{name}Probability"] = {"type": "number", "min": 0, "max": 100}
70
+
71
+ colors = self._style.colors()
72
+ color_names = [*colors.keys(), "background"]
73
+
74
+ for name in color_names:
75
+ color_field: dict[str, Any] = {"type": "color", "list": True}
76
+ contrast_to = colors[name].contrast_to() if name in colors else None
77
+
78
+ if contrast_to is not None:
79
+ color_field["contrastTo"] = contrast_to
80
+
81
+ result[f"{name}Color"] = color_field
82
+ result[f"{name}ColorFill"] = {
83
+ "type": "enum",
84
+ "values": ["solid", "linear", "radial"],
85
+ "list": True,
86
+ }
87
+ result[f"{name}ColorFillStops"] = {"type": "range", "min": 2}
88
+ result[f"{name}ColorAngle"] = dict(_ROTATE_RANGE)
89
+
90
+ return result
@@ -0,0 +1,186 @@
1
+ """Key-based pseudorandom number generator and its primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ import math
7
+ from collections.abc import Callable, Sequence
8
+ from typing import TypeVar
9
+
10
+ from ..utils.number import Number
11
+ from .fnv1a import Fnv1a
12
+ from .mulberry32 import Mulberry32
13
+
14
+ __all__ = ["Fnv1a", "Mulberry32", "Prng"]
15
+
16
+ T = TypeVar("T")
17
+
18
+ # The bool() / float() methods below shadow the builtins for the class scope, so
19
+ # signature annotations that need the builtin types reference them via builtins.
20
+ _Float = builtins.float
21
+ _Bool = builtins.bool
22
+
23
+ Numeric = int | float
24
+ Range = dict[str, Numeric]
25
+
26
+
27
+ class Prng:
28
+ """Key-based pseudorandom number generator.
29
+
30
+ Each method takes a key that, combined with the seed, produces a
31
+ deterministic value. The same seed + key always yields the same result,
32
+ regardless of call order.
33
+ """
34
+
35
+ def __init__(self, seed: str) -> None:
36
+ self._seed = seed
37
+
38
+ def pick(self, key: str, items: Sequence[T]) -> T | None:
39
+ """Pick a single item from ``items`` deterministically.
40
+
41
+ Returns ``None`` for an empty list. Duplicate values (by string
42
+ representation) are collapsed before picking so that input order and
43
+ duplication do not affect the result.
44
+ """
45
+ if len(items) == 0:
46
+ return None
47
+
48
+ if len(items) == 1:
49
+ return items[0]
50
+
51
+ unique = _unique_by_code_point(items)
52
+
53
+ if len(unique) == 1:
54
+ return unique[0]
55
+
56
+ ordered = sorted(unique, key=_code_point_key)
57
+ index = math.floor(self.get_value(key) * len(ordered))
58
+
59
+ return ordered[index]
60
+
61
+ def weighted_pick(self, key: str, weights: dict[str, Numeric]) -> str | None:
62
+ """Pick a key from ``weights`` proportional to its weight.
63
+
64
+ When all weights are zero, falls back to an unweighted :meth:`pick`.
65
+ Returns ``None`` for an empty map.
66
+ """
67
+ if len(weights) == 0:
68
+ return None
69
+
70
+ keys = list(weights.keys())
71
+
72
+ if len(keys) == 1:
73
+ return str(keys[0])
74
+
75
+ keys = sorted(keys, key=_code_point_key)
76
+
77
+ # Sum in sorted-key order to match JS reduce-over-sorted parity — float
78
+ # addition is non-associative, so iterating in insertion order would
79
+ # diverge from JS for fractional weights.
80
+ total_weight: float = 0.0
81
+ for k in keys:
82
+ total_weight += weights[k]
83
+
84
+ if total_weight == 0.0:
85
+ return self.pick(key, [str(k) for k in keys])
86
+
87
+ threshold = self.get_value(key) * total_weight
88
+ cumulative: float = 0
89
+
90
+ for k in keys:
91
+ cumulative += weights[k]
92
+
93
+ if threshold < cumulative:
94
+ return str(k)
95
+
96
+ return str(keys[-1])
97
+
98
+ def bool(self, key: str, likelihood: _Float = 50) -> _Bool:
99
+ """Return ``True`` with the given probability (0–100, default 50)."""
100
+ return self.get_value(key) * 100 < likelihood
101
+
102
+ def float(self, key: str, range: Range) -> _Float:
103
+ """Return a deterministic float in ``range``, rounded to four decimals.
104
+
105
+ With ``range['step'] > 0``, the result is drawn uniformly from
106
+ ``{ min + i*step | 0 <= i <= floor((max - min) / step) }``, so both
107
+ endpoints of an evenly-divisible range are equally likely. Non-positive
108
+ or absent step means continuous. ``min``/``max`` are sorted internally,
109
+ so a reversed pair is tolerated.
110
+ """
111
+ low = min(range["min"], range["max"])
112
+ high = max(range["min"], range["max"])
113
+ step = range.get("step", 0)
114
+
115
+ if step > 0:
116
+ buckets = math.floor((high - low) / step) + 1
117
+ i = math.floor(self.get_value(key) * buckets)
118
+ value = low + i * step
119
+ else:
120
+ value = low + self.get_value(key) * (high - low)
121
+
122
+ return Number.round_half_up(value * 10000) / 10000
123
+
124
+ def integer(self, key: str, range: Range) -> int:
125
+ """Return a deterministic integer in ``range``.
126
+
127
+ ``min``/``max`` are sorted internally, so a reversed pair is tolerated.
128
+ ``range['step']`` is accepted for symmetry with :meth:`float` but ignored.
129
+ """
130
+ low = int(min(range["min"], range["max"]))
131
+ high = int(max(range["min"], range["max"]))
132
+
133
+ return math.floor(self.get_value(key) * (high - low + 1)) + low
134
+
135
+ def shuffle(self, key: str, items: Sequence[T]) -> list[T]:
136
+ """Fisher-Yates shuffle with chained Mulberry32 state.
137
+
138
+ Duplicate values (by string representation) are collapsed before
139
+ shuffling, so a caller's slice off the front cannot accidentally produce
140
+ a repeated value.
141
+ """
142
+ if len(items) <= 1:
143
+ return list(items)
144
+
145
+ result = sorted(_unique_by_code_point(items), key=_code_point_key)
146
+ prng = Mulberry32(Fnv1a.hash(self._seed + ":" + key))
147
+
148
+ for i in range(len(result) - 1, 0, -1):
149
+ j = math.floor(prng.next_float() * (i + 1))
150
+ result[i], result[j] = result[j], result[i]
151
+
152
+ return result
153
+
154
+ def get_value(self, key: str) -> _Float:
155
+ """Return a single float in ``[0, 1)`` derived from ``seed:key``.
156
+
157
+ The same seed/key pair always produces the same value.
158
+ """
159
+ return Mulberry32(Fnv1a.hash(self._seed + ":" + key)).next_float()
160
+
161
+
162
+ def _code_point_key(value: object) -> str:
163
+ """Cross-language deterministic sort key.
164
+
165
+ Compare by the string representation's code points, matching JS
166
+ ``String(a) < String(b)`` and PHP ``strcmp`` for the ASCII values sorted in
167
+ practice (variant names, hex colors).
168
+ """
169
+ return str(value)
170
+
171
+
172
+ def _unique_by_code_point(
173
+ items: Sequence[T], key_fn: Callable[[T], str] = str
174
+ ) -> list[T]:
175
+ """Deduplicate by string representation, keeping the first occurrence."""
176
+ seen: set[str] = set()
177
+ result: list[T] = []
178
+
179
+ for item in items:
180
+ repr_ = key_fn(item)
181
+
182
+ if repr_ not in seen:
183
+ seen.add(repr_)
184
+ result.append(item)
185
+
186
+ return result
dicebear/prng/fnv1a.py ADDED
@@ -0,0 +1,47 @@
1
+ """FNV-1a 32-bit hash."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+
7
+
8
+ class Fnv1a:
9
+ """FNV-1a 32-bit hash.
10
+
11
+ Offset basis: ``0x811c9dc5``, prime: ``0x01000193``.
12
+
13
+ See https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
14
+ """
15
+
16
+ @staticmethod
17
+ def hash(value: str) -> int:
18
+ """Return the unsigned 32-bit FNV-1a hash of ``value``.
19
+
20
+ The input is decomposed into UTF-16 code units before hashing so the
21
+ result is identical across language ports.
22
+ """
23
+ result = 0x811C9DC5
24
+
25
+ for code in _utf16_code_units(value):
26
+ result = ((result ^ code) * 0x01000193) & 0xFFFFFFFF
27
+
28
+ return result
29
+
30
+ @staticmethod
31
+ def hex(value: str) -> str:
32
+ """Return the FNV-1a hash of ``value`` as an 8-char lowercase hex string."""
33
+ return format(Fnv1a.hash(value), "08x")
34
+
35
+
36
+ def _utf16_code_units(value: str) -> list[int]:
37
+ """Convert a string to its UTF-16 code units, matching JS ``charCodeAt``.
38
+
39
+ Iterating ``value`` directly would yield code points and break on non-BMP
40
+ input (e.g. emoji), so encode to little-endian UTF-16 and read uint16 units.
41
+ """
42
+ if value == "":
43
+ return []
44
+
45
+ encoded = value.encode("utf-16-le")
46
+
47
+ return list(struct.unpack(f"<{len(encoded) // 2}H", encoded))
@@ -0,0 +1,57 @@
1
+ """Mulberry32 PRNG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _UINT32_MAX_PLUS_1 = 2**32
6
+
7
+
8
+ class Mulberry32:
9
+ """Mulberry32 PRNG — stateful, matching the C reference by Tommy Ettinger.
10
+
11
+ C original::
12
+
13
+ uint32_t z = (x += 0x6D2B79F5UL);
14
+ z = (z ^ (z >> 15)) * (z | 1UL);
15
+ z ^= z + (z ^ (z >> 7)) * (z | 61UL);
16
+ return z ^ (z >> 14);
17
+
18
+ All arithmetic is unsigned 32-bit. Python ints are arbitrary precision, so
19
+ every operation is masked with ``& 0xFFFFFFFF`` to emulate JS ``Math.imul``
20
+ / ``>>>`` and keep parity with the JS reference.
21
+
22
+ See https://gist.github.com/tommyettinger/46a874533244883189143505d203312c
23
+ """
24
+
25
+ def __init__(self, seed: int = 0) -> None:
26
+ self._state = seed & 0xFFFFFFFF
27
+
28
+ def next(self) -> int:
29
+ """Advance the state and return the next unsigned 32-bit value."""
30
+ z = (self._state + 0x6D2B79F5) & 0xFFFFFFFF
31
+ self._state = z
32
+
33
+ z = _mul(z ^ (z >> 15), z | 1)
34
+ z ^= _add(z, _mul(z ^ (z >> 7), z | 61))
35
+
36
+ return (z ^ (z >> 14)) & 0xFFFFFFFF
37
+
38
+ def next_float(self) -> float:
39
+ """Advance the state and return the next value in ``[0, 1)``."""
40
+ return self.next() / _UINT32_MAX_PLUS_1
41
+
42
+ def state(self) -> int:
43
+ """Return the current state as a signed 32-bit value (for JS compat)."""
44
+ if self._state >= 0x80000000:
45
+ return self._state - _UINT32_MAX_PLUS_1
46
+
47
+ return self._state
48
+
49
+
50
+ def _mul(a: int, b: int) -> int:
51
+ """Unsigned 32-bit multiply (low 32 bits), matching JS ``Math.imul``."""
52
+ return (a * b) & 0xFFFFFFFF
53
+
54
+
55
+ def _add(a: int, b: int) -> int:
56
+ """Unsigned 32-bit add."""
57
+ return (a + b) & 0xFFFFFFFF
dicebear/py.typed ADDED
File without changes