humanized-hash 1.0.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.
- humanized_hash/__init__.py +56 -0
- humanized_hash/__main__.py +10 -0
- humanized_hash/_api.py +388 -0
- humanized_hash/_bmp.py +38 -0
- humanized_hash/_cli.py +514 -0
- humanized_hash/_contrast.py +71 -0
- humanized_hash/_derive.py +145 -0
- humanized_hash/_errors.py +63 -0
- humanized_hash/_flatten.py +31 -0
- humanized_hash/_image.py +162 -0
- humanized_hash/_jpeg.py +360 -0
- humanized_hash/_model.py +185 -0
- humanized_hash/_png.py +184 -0
- humanized_hash/_raster.py +396 -0
- humanized_hash/_version.py +5 -0
- humanized_hash/py.typed +0 -0
- humanized_hash-1.0.0.dist-info/METADATA +191 -0
- humanized_hash-1.0.0.dist-info/RECORD +22 -0
- humanized_hash-1.0.0.dist-info/WHEEL +5 -0
- humanized_hash-1.0.0.dist-info/entry_points.txt +2 -0
- humanized_hash-1.0.0.dist-info/licenses/LICENSE +21 -0
- humanized_hash-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Humanized Hash (hh): an address, a public key or a hash as a small deterministic picture.
|
|
2
|
+
|
|
3
|
+
A 4 x 4 matrix of solid squares, circles and triangles in four colours that a person can compare
|
|
4
|
+
at a glance, made to catch address poisoning and clipboard substitution::
|
|
5
|
+
|
|
6
|
+
from humanized_hash import BaseDigest, Fingerprint
|
|
7
|
+
|
|
8
|
+
digest = BaseDigest.of_hex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") # slow: cache it
|
|
9
|
+
fingerprint = Fingerprint.universal(digest) # or Fingerprint.keyed(digest, key)
|
|
10
|
+
image = fingerprint.render(128) # 128 x 128 RGBA pixels
|
|
11
|
+
image.save("address.png")
|
|
12
|
+
fingerprint.tag # "TKSPVH", shown as TKS-PVH
|
|
13
|
+
|
|
14
|
+
Every output is byte-identical with hh-cpp, the reference implementation, which owns the
|
|
15
|
+
specification. The library uses integer arithmetic and the standard library only.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from ._api import BaseDigest, Fingerprint, SecretKey
|
|
21
|
+
from ._errors import ErrorCode, HhError
|
|
22
|
+
from ._image import DEFAULT_JPEG_QUALITY, MAX_DIMENSION, Image
|
|
23
|
+
from ._model import (
|
|
24
|
+
Cell,
|
|
25
|
+
ContrastReport,
|
|
26
|
+
Figure,
|
|
27
|
+
FrameStyle,
|
|
28
|
+
Layout,
|
|
29
|
+
Mode,
|
|
30
|
+
RenderOptions,
|
|
31
|
+
Shape,
|
|
32
|
+
)
|
|
33
|
+
from ._raster import MAX_SIZE, MIN_SIZE
|
|
34
|
+
from ._version import __version__
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_JPEG_QUALITY",
|
|
38
|
+
"MAX_DIMENSION",
|
|
39
|
+
"MAX_SIZE",
|
|
40
|
+
"MIN_SIZE",
|
|
41
|
+
"BaseDigest",
|
|
42
|
+
"Cell",
|
|
43
|
+
"ContrastReport",
|
|
44
|
+
"ErrorCode",
|
|
45
|
+
"Figure",
|
|
46
|
+
"Fingerprint",
|
|
47
|
+
"FrameStyle",
|
|
48
|
+
"HhError",
|
|
49
|
+
"Image",
|
|
50
|
+
"Layout",
|
|
51
|
+
"Mode",
|
|
52
|
+
"RenderOptions",
|
|
53
|
+
"SecretKey",
|
|
54
|
+
"Shape",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
humanized_hash/_api.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""The base digest, the secret key and the fingerprint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import operator
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any, Callable, NoReturn, Optional, Tuple, Type, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
from . import _contrast, _derive, _raster
|
|
10
|
+
from ._errors import ErrorCode, HhError
|
|
11
|
+
from ._image import Image
|
|
12
|
+
from ._model import DEFAULT_OPTIONS, Layout, Mode, RenderOptions, as_bytes
|
|
13
|
+
|
|
14
|
+
_Bytes = Union[bytes, bytearray, memoryview]
|
|
15
|
+
_T = TypeVar("_T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseDigest:
|
|
19
|
+
"""The base digest of an input: its stretched, public 32-byte value (SPEC.md section 4).
|
|
20
|
+
|
|
21
|
+
It is the only slow step, 16 384 iterations of PBKDF2-HMAC-SHA-256, so hosts compute it off
|
|
22
|
+
the event loop and cache ``bytes(digest)`` per address; both modes and any key derive their
|
|
23
|
+
fingerprint from it cheaply. It is public and needs no protection.
|
|
24
|
+
|
|
25
|
+
Compute one with :meth:`of`, :meth:`of_hex` or :meth:`of_text`; :meth:`from_bytes` computes
|
|
26
|
+
nothing and only restores a digest that was stored. A digest is immutable, hashable and
|
|
27
|
+
compares by value.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
SIZE = 32
|
|
31
|
+
"""The size of a base digest in bytes."""
|
|
32
|
+
|
|
33
|
+
__slots__ = ("_bytes",)
|
|
34
|
+
_bytes: bytes
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
37
|
+
raise TypeError(
|
|
38
|
+
"use BaseDigest.of(), of_hex() or of_text(), or from_bytes() for a stored digest"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _wrap(cls, raw: bytes) -> BaseDigest:
|
|
43
|
+
self = object.__new__(cls)
|
|
44
|
+
object.__setattr__(self, "_bytes", raw)
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def of(cls, data: _Bytes) -> BaseDigest:
|
|
49
|
+
"""Binary input: the bytes of an address, a public key or a hash, 1 to 1 048 576.
|
|
50
|
+
|
|
51
|
+
Raises :class:`HhError` with ``EMPTY_INPUT`` or ``INPUT_TOO_LARGE``.
|
|
52
|
+
"""
|
|
53
|
+
return cls._wrap(_derive.base_digest(_derive.KIND_BINARY, as_bytes(data, "data")))
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def of_hex(cls, text: str) -> BaseDigest:
|
|
57
|
+
"""Binary input given as hexadecimal text.
|
|
58
|
+
|
|
59
|
+
An optional ``0x`` or ``0X``, then an even, non-zero number of hexadecimal digits of
|
|
60
|
+
either case. Every spelling of one address gives the same digest.
|
|
61
|
+
|
|
62
|
+
Raises :class:`HhError` with ``INVALID_HEX`` or, for well-formed text that decodes to
|
|
63
|
+
more than 1 048 576 bytes, ``INPUT_TOO_LARGE``.
|
|
64
|
+
"""
|
|
65
|
+
if not isinstance(text, str):
|
|
66
|
+
raise TypeError(f"text must be a str, not {type(text).__name__}")
|
|
67
|
+
return cls._wrap(_derive.base_digest(_derive.KIND_BINARY, _derive.decode_hex(text)))
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def of_text(cls, text: Union[str, _Bytes]) -> BaseDigest:
|
|
71
|
+
"""Text input: the UTF-8 encoding of ``text``, verbatim, without normalisation.
|
|
72
|
+
|
|
73
|
+
A ``str`` is encoded here; bytes are taken as UTF-8 that is already encoded and are not
|
|
74
|
+
validated. A text input and a binary input with the same bytes give different digests.
|
|
75
|
+
|
|
76
|
+
Raises :class:`HhError` with ``EMPTY_INPUT``, ``INPUT_TOO_LARGE`` or, for a ``str`` that
|
|
77
|
+
holds a lone surrogate, ``INVALID_ARGUMENT``.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(text, str):
|
|
80
|
+
data = _derive.encode_text(text)
|
|
81
|
+
else:
|
|
82
|
+
data = as_bytes(text, "text")
|
|
83
|
+
return cls._wrap(_derive.base_digest(_derive.KIND_TEXT, data))
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_bytes(cls, raw: _Bytes) -> BaseDigest:
|
|
87
|
+
"""Not for an address, a public key or a hash: those go to :meth:`of`.
|
|
88
|
+
|
|
89
|
+
Restores a digest that was computed before and stored, for example ``bytes(digest)``
|
|
90
|
+
from a cache. Nothing is computed here: the 32 bytes of an address would be accepted
|
|
91
|
+
and give a picture that is not the picture of that address.
|
|
92
|
+
|
|
93
|
+
Raises :class:`HhError` with ``INVALID_DIGEST`` unless ``raw`` has 32 bytes.
|
|
94
|
+
"""
|
|
95
|
+
data = as_bytes(raw, "raw")
|
|
96
|
+
if len(data) != cls.SIZE:
|
|
97
|
+
raise HhError(ErrorCode.INVALID_DIGEST, f"a base digest has {cls.SIZE} bytes")
|
|
98
|
+
return cls._wrap(data)
|
|
99
|
+
|
|
100
|
+
def hex(self) -> str:
|
|
101
|
+
"""The 32 bytes in lowercase hexadecimal."""
|
|
102
|
+
return self._bytes.hex()
|
|
103
|
+
|
|
104
|
+
def __bytes__(self) -> bytes:
|
|
105
|
+
return self._bytes
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other: object) -> bool:
|
|
108
|
+
if not isinstance(other, BaseDigest):
|
|
109
|
+
return NotImplemented
|
|
110
|
+
return self._bytes == other._bytes
|
|
111
|
+
|
|
112
|
+
def __hash__(self) -> int:
|
|
113
|
+
return hash(self._bytes)
|
|
114
|
+
|
|
115
|
+
def __setattr__(self, name: str, value: Any) -> NoReturn:
|
|
116
|
+
raise AttributeError("a BaseDigest is immutable")
|
|
117
|
+
|
|
118
|
+
def __delattr__(self, name: str) -> NoReturn:
|
|
119
|
+
raise AttributeError("a BaseDigest is immutable")
|
|
120
|
+
|
|
121
|
+
def __reduce__(self) -> Tuple[Any, ...]:
|
|
122
|
+
return (BaseDigest.from_bytes, (self._bytes,))
|
|
123
|
+
|
|
124
|
+
def __repr__(self) -> str:
|
|
125
|
+
return f"BaseDigest.from_bytes(bytes.fromhex('{self._bytes.hex()}'))"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _KeyBuffer(bytearray):
|
|
129
|
+
"""The key bytes of a :class:`SecretKey`.
|
|
130
|
+
|
|
131
|
+
A ``bytearray`` that ``hmac`` takes as it is and that never prints its content, so that a
|
|
132
|
+
traceback or a debugger that shows local variables shows no key.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
__slots__ = ()
|
|
136
|
+
|
|
137
|
+
def __repr__(self) -> str:
|
|
138
|
+
return "<key bytes>"
|
|
139
|
+
|
|
140
|
+
__str__ = __repr__
|
|
141
|
+
|
|
142
|
+
def __reduce_ex__(self, protocol: Any) -> NoReturn:
|
|
143
|
+
raise TypeError("key bytes cannot be pickled or copied")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SecretKey:
|
|
147
|
+
"""The 32-byte secret of keyed mode.
|
|
148
|
+
|
|
149
|
+
The key must be uniformly random or the output of a key derivation function: there is no
|
|
150
|
+
passphrase form. Exactly 32 bytes that are not all zero are accepted, from any bytes-like
|
|
151
|
+
object; the bytes are copied and no reference to the object is kept, so wipe your own buffer.
|
|
152
|
+
|
|
153
|
+
:meth:`close` overwrites the copy held here with zeros; use the key as a context manager or
|
|
154
|
+
close it when the application locks. A closed key refuses every use with ``INVALID_KEY``;
|
|
155
|
+
only :attr:`check_value`, which is public, stays readable.
|
|
156
|
+
|
|
157
|
+
That is as far as Python allows: the bytes you passed in, if they are an immutable ``bytes``
|
|
158
|
+
object, and the copies that ``hmac`` and the interpreter make while computing stay in memory
|
|
159
|
+
until the allocator reuses it. Hosts that cannot accept that compute the keyed fingerprint
|
|
160
|
+
in native code or a secure element and hand the result to :meth:`Fingerprint.from_bytes`.
|
|
161
|
+
|
|
162
|
+
A key cannot be pickled or copied, and its ``repr`` never shows key material.
|
|
163
|
+
|
|
164
|
+
Raises :class:`HhError` with ``INVALID_KEY``.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
SIZE = 32
|
|
168
|
+
"""The size of a key in bytes."""
|
|
169
|
+
|
|
170
|
+
__slots__ = ("_bytes", "_check_value", "_closed")
|
|
171
|
+
_bytes: _KeyBuffer
|
|
172
|
+
_check_value: bytes
|
|
173
|
+
_closed: bool
|
|
174
|
+
|
|
175
|
+
def __init__(self, data: _Bytes) -> None:
|
|
176
|
+
self._closed = True
|
|
177
|
+
self._bytes = _KeyBuffer(self.SIZE)
|
|
178
|
+
self._check_value = b""
|
|
179
|
+
if not isinstance(data, (bytes, bytearray, memoryview)):
|
|
180
|
+
raise TypeError(f"data must be bytes-like, not {type(data).__name__}")
|
|
181
|
+
try:
|
|
182
|
+
view = memoryview(data)
|
|
183
|
+
except ValueError:
|
|
184
|
+
raise TypeError("data is a released memoryview") from None
|
|
185
|
+
# Whatever is raised below, this frame holds no name for the caller's bytes, and the view
|
|
186
|
+
# is released first: the caller can wipe and resize its buffer.
|
|
187
|
+
del data
|
|
188
|
+
with view:
|
|
189
|
+
fits = view.nbytes == self.SIZE
|
|
190
|
+
if fits and view.contiguous:
|
|
191
|
+
# Straight into the buffer that close() wipes, without a copy in between.
|
|
192
|
+
with view.cast("B") as flat:
|
|
193
|
+
self._bytes[:] = flat
|
|
194
|
+
elif fits:
|
|
195
|
+
self._bytes[:] = view.tobytes()
|
|
196
|
+
if not fits:
|
|
197
|
+
raise HhError(ErrorCode.INVALID_KEY, f"a key has {self.SIZE} bytes")
|
|
198
|
+
if not any(self._bytes):
|
|
199
|
+
raise HhError(ErrorCode.INVALID_KEY, "the all-zero key is not a key")
|
|
200
|
+
self._check_value = _derive.key_check_value(self._bytes)
|
|
201
|
+
self._closed = False
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def closed(self) -> bool:
|
|
205
|
+
"""Whether :meth:`close` was called."""
|
|
206
|
+
return self._closed
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def check_value(self) -> bytes:
|
|
210
|
+
"""The key check value: 4 bytes a host stores beside its cached data.
|
|
211
|
+
|
|
212
|
+
A different value means that the key, and with it every keyed picture, changed. The
|
|
213
|
+
value is public and is computed when the key is created, so it stays readable after
|
|
214
|
+
:meth:`close`.
|
|
215
|
+
"""
|
|
216
|
+
return self._check_value
|
|
217
|
+
|
|
218
|
+
def _use(self, function: Callable[..., _T], *args: Any) -> _T:
|
|
219
|
+
"""Calls ``function(key bytes, *args)``.
|
|
220
|
+
|
|
221
|
+
A key that is closed meanwhile is wiped to zeros, and a result computed under zeros must
|
|
222
|
+
never be handed out, so the key is checked again afterwards.
|
|
223
|
+
"""
|
|
224
|
+
self._ensure_open()
|
|
225
|
+
result = function(self._bytes, *args)
|
|
226
|
+
self._ensure_open()
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
def _ensure_open(self) -> None:
|
|
230
|
+
if self._closed:
|
|
231
|
+
raise HhError(ErrorCode.INVALID_KEY, "the key was closed")
|
|
232
|
+
|
|
233
|
+
def close(self) -> None:
|
|
234
|
+
"""Wipes the key. Closing twice is harmless; using a closed key raises ``INVALID_KEY``."""
|
|
235
|
+
self._closed = True
|
|
236
|
+
for i in range(len(self._bytes)):
|
|
237
|
+
self._bytes[i] = 0
|
|
238
|
+
|
|
239
|
+
def __enter__(self) -> SecretKey:
|
|
240
|
+
self._ensure_open()
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def __exit__(
|
|
244
|
+
self,
|
|
245
|
+
exc_type: Optional[Type[BaseException]],
|
|
246
|
+
exc: Optional[BaseException],
|
|
247
|
+
traceback: Optional[TracebackType],
|
|
248
|
+
) -> None:
|
|
249
|
+
self.close()
|
|
250
|
+
|
|
251
|
+
def __del__(self) -> None:
|
|
252
|
+
try:
|
|
253
|
+
self.close()
|
|
254
|
+
except AttributeError:
|
|
255
|
+
# The constructor failed before the buffer existed.
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
def __reduce__(self) -> NoReturn:
|
|
259
|
+
raise TypeError("a SecretKey cannot be pickled or copied")
|
|
260
|
+
|
|
261
|
+
def __repr__(self) -> str:
|
|
262
|
+
return "SecretKey(***)"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class Fingerprint:
|
|
266
|
+
"""32 bytes and the mode they were derived in: everything a picture depends on.
|
|
267
|
+
|
|
268
|
+
Create one with :meth:`universal` or :meth:`keyed`, or import bytes computed elsewhere with
|
|
269
|
+
:meth:`from_bytes`. A fingerprint is immutable, hashable and compares by bytes and mode.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
SIZE = 32
|
|
273
|
+
"""The size of a fingerprint in bytes."""
|
|
274
|
+
|
|
275
|
+
__slots__ = ("_bytes", "_mode")
|
|
276
|
+
_bytes: bytes
|
|
277
|
+
_mode: Mode
|
|
278
|
+
|
|
279
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
280
|
+
raise TypeError("use Fingerprint.universal(), keyed() or from_bytes()")
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def _wrap(cls, raw: bytes, mode: Mode) -> Fingerprint:
|
|
284
|
+
self = object.__new__(cls)
|
|
285
|
+
object.__setattr__(self, "_bytes", raw)
|
|
286
|
+
object.__setattr__(self, "_mode", mode)
|
|
287
|
+
return self
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def universal(cls, digest: BaseDigest) -> Fingerprint:
|
|
291
|
+
"""The universal fingerprint is the base digest itself."""
|
|
292
|
+
if not isinstance(digest, BaseDigest):
|
|
293
|
+
raise TypeError(f"digest must be a BaseDigest, not {type(digest).__name__}")
|
|
294
|
+
return cls._wrap(bytes(digest), Mode.UNIVERSAL)
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def keyed(cls, digest: BaseDigest, key: SecretKey) -> Fingerprint:
|
|
298
|
+
"""The keyed fingerprint: one HMAC of the base digest under the key.
|
|
299
|
+
|
|
300
|
+
Raises :class:`HhError` with ``INVALID_KEY`` if ``key`` was closed.
|
|
301
|
+
"""
|
|
302
|
+
if not isinstance(digest, BaseDigest):
|
|
303
|
+
raise TypeError(f"digest must be a BaseDigest, not {type(digest).__name__}")
|
|
304
|
+
if not isinstance(key, SecretKey):
|
|
305
|
+
raise TypeError(f"key must be a SecretKey, not {type(key).__name__}")
|
|
306
|
+
return cls._wrap(key._use(_derive.keyed_fingerprint, bytes(digest)), Mode.KEYED)
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_bytes(cls, raw: _Bytes, mode: Union[Mode, int]) -> Fingerprint:
|
|
310
|
+
"""Takes the 32 fingerprint bytes and the mode they belong to.
|
|
311
|
+
|
|
312
|
+
For hosts that compute the keyed HMAC elsewhere, for example in native code or inside a
|
|
313
|
+
secure element. ``mode`` is a :class:`Mode` or its value, the ``int`` 1 or 2; a ``bool``
|
|
314
|
+
or a ``float`` is a ``TypeError``.
|
|
315
|
+
|
|
316
|
+
Raises :class:`HhError` with ``INVALID_FINGERPRINT`` unless ``raw`` has 32 bytes and the
|
|
317
|
+
mode is known.
|
|
318
|
+
"""
|
|
319
|
+
data = as_bytes(raw, "raw")
|
|
320
|
+
if not isinstance(mode, int) or isinstance(mode, bool):
|
|
321
|
+
raise TypeError(f"mode must be a Mode or an int, not {type(mode).__name__}")
|
|
322
|
+
try:
|
|
323
|
+
known = Mode(mode)
|
|
324
|
+
except ValueError:
|
|
325
|
+
raise HhError(ErrorCode.INVALID_FINGERPRINT, "the mode is 1 or 2") from None
|
|
326
|
+
if len(data) != cls.SIZE:
|
|
327
|
+
raise HhError(ErrorCode.INVALID_FINGERPRINT, f"a fingerprint has {cls.SIZE} bytes")
|
|
328
|
+
return cls._wrap(data, known)
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def mode(self) -> Mode:
|
|
332
|
+
"""The mode the bytes were derived in."""
|
|
333
|
+
return self._mode
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def tag(self) -> str:
|
|
337
|
+
"""The six-character Crockford Base32 tag, for example ``K7QM2X``.
|
|
338
|
+
|
|
339
|
+
Hosts display it as ``K7Q-M2X``. Text allows a certain check where a picture does not.
|
|
340
|
+
"""
|
|
341
|
+
return _raster.tag(self._bytes)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def layout(self) -> Layout:
|
|
345
|
+
"""The cells, the palette and the mode, for hosts that draw vectors themselves."""
|
|
346
|
+
return Layout(
|
|
347
|
+
self._mode, _raster.cells(self._bytes), _contrast.PALETTE, _contrast.FRAME_COLOUR
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def render(self, size: int, options: Optional[RenderOptions] = None) -> Image:
|
|
351
|
+
"""Renders ``size`` x ``size`` pixels, 16..1024.
|
|
352
|
+
|
|
353
|
+
Raises :class:`HhError` with ``INVALID_SIZE``, ``INVALID_FRAME`` or ``LOW_CONTRAST``, as
|
|
354
|
+
section 6 of the specification defines, in that order of checks.
|
|
355
|
+
"""
|
|
356
|
+
size = operator.index(size)
|
|
357
|
+
if options is None:
|
|
358
|
+
options = DEFAULT_OPTIONS
|
|
359
|
+
elif not isinstance(options, RenderOptions):
|
|
360
|
+
raise TypeError(f"options must be RenderOptions, not {type(options).__name__}")
|
|
361
|
+
return Image(size, size, _raster.render(self._bytes, self._mode, size, options))
|
|
362
|
+
|
|
363
|
+
def hex(self) -> str:
|
|
364
|
+
"""The 32 bytes in lowercase hexadecimal."""
|
|
365
|
+
return self._bytes.hex()
|
|
366
|
+
|
|
367
|
+
def __bytes__(self) -> bytes:
|
|
368
|
+
return self._bytes
|
|
369
|
+
|
|
370
|
+
def __eq__(self, other: object) -> bool:
|
|
371
|
+
if not isinstance(other, Fingerprint):
|
|
372
|
+
return NotImplemented
|
|
373
|
+
return self._mode is other._mode and self._bytes == other._bytes
|
|
374
|
+
|
|
375
|
+
def __hash__(self) -> int:
|
|
376
|
+
return hash((self._mode, self._bytes))
|
|
377
|
+
|
|
378
|
+
def __setattr__(self, name: str, value: Any) -> NoReturn:
|
|
379
|
+
raise AttributeError("a Fingerprint is immutable")
|
|
380
|
+
|
|
381
|
+
def __delattr__(self, name: str) -> NoReturn:
|
|
382
|
+
raise AttributeError("a Fingerprint is immutable")
|
|
383
|
+
|
|
384
|
+
def __reduce__(self) -> Tuple[Any, ...]:
|
|
385
|
+
return (Fingerprint.from_bytes, (self._bytes, int(self._mode)))
|
|
386
|
+
|
|
387
|
+
def __repr__(self) -> str:
|
|
388
|
+
return f"<Fingerprint {self._mode.name.lower()} {self.tag}>"
|
humanized_hash/_bmp.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""BMP: SPEC.md section 12."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._flatten import flatten
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def encode(width: int, height: int, rgba: bytes, matte: int) -> bytes:
|
|
9
|
+
"""A bottom-up 24-bit ``BI_RGB`` file of the pixels flattened over the matte."""
|
|
10
|
+
row = 4 * ((3 * width + 3) // 4)
|
|
11
|
+
size = row * height
|
|
12
|
+
header = b"".join(
|
|
13
|
+
(
|
|
14
|
+
b"BM",
|
|
15
|
+
(54 + size).to_bytes(4, "little"),
|
|
16
|
+
bytes(4),
|
|
17
|
+
(54).to_bytes(4, "little"),
|
|
18
|
+
(40).to_bytes(4, "little"),
|
|
19
|
+
width.to_bytes(4, "little"),
|
|
20
|
+
height.to_bytes(4, "little"),
|
|
21
|
+
(1).to_bytes(2, "little"),
|
|
22
|
+
(24).to_bytes(2, "little"),
|
|
23
|
+
bytes(4),
|
|
24
|
+
size.to_bytes(4, "little"),
|
|
25
|
+
(2835).to_bytes(4, "little"),
|
|
26
|
+
(2835).to_bytes(4, "little"),
|
|
27
|
+
bytes(8),
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
red, green, blue = flatten(rgba, matte)
|
|
31
|
+
out = bytearray(size)
|
|
32
|
+
for y in range(height):
|
|
33
|
+
source = slice(y * width, (y + 1) * width)
|
|
34
|
+
at = (height - 1 - y) * row
|
|
35
|
+
out[at : at + 3 * width : 3] = blue[source]
|
|
36
|
+
out[at + 1 : at + 3 * width : 3] = green[source]
|
|
37
|
+
out[at + 2 : at + 3 * width : 3] = red[source]
|
|
38
|
+
return header + bytes(out)
|