python-libphash 2.0.1.0__cp315-cp315t-win_amd64.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.
libphash/_sources.py ADDED
@@ -0,0 +1,278 @@
1
+ """Turning what a caller has into C arguments.
2
+
3
+ A caller has a path, bytes, an array of pixels or a Pillow image.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, Union, cast, runtime_checkable
12
+
13
+ from ._errors import PH_ERR_IO, make_error
14
+ from ._native import ffi
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Mapping
18
+
19
+ from typing_extensions import Buffer
20
+
21
+ from ._native import char_ptr, uint8_t_ptr
22
+
23
+ #: A file system path, as ``open()`` takes it.
24
+ StrPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
25
+ #: Order of the color channels in an array of pixels: ``"bgr"`` is OpenCV's.
26
+ ChannelOrder = Literal["rgb", "bgr"]
27
+
28
+
29
+ class SupportsRead(Protocol):
30
+ """A binary file object: an open file, ``io.BytesIO``, a response body."""
31
+
32
+ def read(self) -> bytes: ...
33
+
34
+
35
+ @runtime_checkable
36
+ class SupportsArrayInterface(Protocol):
37
+ """An array exposing numpy's ``__array_interface__``.
38
+
39
+ A numpy array is one, and so is a Pillow image.
40
+ """
41
+
42
+ @property
43
+ def __array_interface__(self) -> Mapping[str, Any]: ...
44
+
45
+
46
+ class PILImage(Protocol):
47
+ """The part of ``PIL.Image.Image`` the binding reads."""
48
+
49
+ @property
50
+ def mode(self) -> str: ...
51
+ @property
52
+ def size(self) -> tuple[int, int]: ...
53
+ def tobytes(self) -> bytes: ...
54
+ def convert(self, mode: str) -> PILImage: ...
55
+
56
+
57
+ #: Decoded 8-bit pixels of shape (h, w) or (h, w, 1|3|4): a numpy array, a memoryview.
58
+ PixelArray = SupportsArrayInterface | memoryview
59
+
60
+ #: Everything :func:`libphash.hash` and :meth:`ImageContext.load` accept: a path,
61
+ #: encoded bytes, a binary file object, an array of pixels or a Pillow image. ``bytes``
62
+ #: are always encoded image data, never a path.
63
+ Source = Union[
64
+ str,
65
+ "os.PathLike[str]",
66
+ "os.PathLike[bytes]",
67
+ bytes,
68
+ bytearray,
69
+ memoryview,
70
+ SupportsArrayInterface,
71
+ SupportsRead,
72
+ ]
73
+
74
+ #: Paths reach the C library's narrow-character open(), which on Windows reads the ANSI
75
+ #: code page, not the UTF-8 that os.fsencode() gives. On Windows the binding therefore
76
+ #: reads the file itself and loads it from memory.
77
+ READ_FILES_IN_PYTHON = sys.platform == "win32"
78
+
79
+
80
+ def encode_path(path: StrPath) -> bytes:
81
+ """Return the path as the bytes the C library gets.
82
+
83
+ They are the file system encoding of the path: on POSIX, the path's bytes unchanged.
84
+ A path with a null byte raises ValueError, as ``open()`` does: C would read only the
85
+ part before it and open another file.
86
+ """
87
+ try:
88
+ encoded = os.fsencode(path)
89
+ except TypeError:
90
+ raise TypeError(
91
+ f"expected a path (str, bytes or os.PathLike), got {type(path).__name__}"
92
+ ) from None
93
+ if b"\0" in encoded:
94
+ raise ValueError("embedded null byte")
95
+ return encoded
96
+
97
+
98
+ def encode_text(text: str) -> bytes:
99
+ """Return a name or a hex form as the ASCII string a C parser reads.
100
+
101
+ C reads up to the first null byte, so a null byte, like any other character outside
102
+ ASCII, becomes ``?``, which no parser of libphash accepts.
103
+ """
104
+ return text.encode("ascii", "replace").replace(b"\0", b"?")
105
+
106
+
107
+ def is_pil_image(obj: object) -> bool:
108
+ module = str(type(obj).__module__)
109
+ return (
110
+ module.startswith("PIL.") and hasattr(obj, "mode") and hasattr(obj, "tobytes")
111
+ )
112
+
113
+
114
+ @dataclass
115
+ class Pixels:
116
+ """Arguments for ph_load_from_pixels().
117
+
118
+ The pointer is either ``ffi.from_buffer()``'s, which keeps its object alive, or an
119
+ array's own address, valid while the caller holds the array.
120
+ """
121
+
122
+ pointer: char_ptr | uint8_t_ptr
123
+ width: int
124
+ height: int
125
+ channels: int
126
+ stride: int
127
+
128
+
129
+ def pixels_from_array(obj: PixelArray, channel_order: ChannelOrder) -> Pixels:
130
+ """Read the pixels of an 8-bit array of shape (h, w) or (h, w, 1|3|4).
131
+
132
+ A numpy array whose rows are spaced apart (a crop, ``arr[10:-10, 5:-5]``) is passed
133
+ without a copy, its row step as the C stride; one with gaps or reversed order inside
134
+ a row, or in BGR order, is copied once.
135
+ """
136
+ if channel_order not in ("rgb", "bgr"):
137
+ raise ValueError(f"channel_order must be 'rgb' or 'bgr', got {channel_order!r}")
138
+ iface = getattr(obj, "__array_interface__", None)
139
+ if iface is not None:
140
+ return _from_array_interface(obj, iface, channel_order)
141
+ try:
142
+ # Any object with the buffer protocol and a shape, beyond what PixelArray names.
143
+ view = memoryview(cast("Buffer", obj))
144
+ except TypeError:
145
+ raise TypeError(
146
+ "expected an array of pixels (numpy, memoryview with a shape), "
147
+ f"got {type(obj).__name__}"
148
+ ) from None
149
+ if view.format not in ("B", "c"):
150
+ raise TypeError(
151
+ f"pixels must be 8-bit unsigned (format 'B'), got format {view.format!r}; "
152
+ "convert the array to uint8 first"
153
+ )
154
+ height, width, channels = _shape(tuple(view.shape or ()))
155
+ if view.c_contiguous and channel_order == "rgb":
156
+ return Pixels(ffi.from_buffer(view), width, height, channels, 0)
157
+ data = view.tobytes()
158
+ return _packed(data, width, height, channels, channel_order)
159
+
160
+
161
+ def _from_array_interface(
162
+ obj: object, iface: Mapping[str, Any], channel_order: ChannelOrder
163
+ ) -> Pixels:
164
+ typestr = iface.get("typestr", "")
165
+ if typestr[1:] != "u1":
166
+ raise TypeError(
167
+ f"pixels must be uint8, got typestr {typestr!r}; "
168
+ "convert the array to uint8 first"
169
+ )
170
+ height, width, channels = _shape(tuple(iface["shape"]))
171
+ strides = iface.get("strides")
172
+ if strides is None: # C-contiguous
173
+ strides = (width * channels, channels, 1)[: len(iface["shape"])]
174
+ row = strides[0]
175
+ pixel = strides[1]
176
+ channel = strides[2] if len(strides) == 3 else 1
177
+ data: object = iface.get("data")
178
+ # numpy states the address as (pointer, read_only); other exporters may not.
179
+ address = cast("tuple[object, ...]", data)[0] if isinstance(data, tuple) else None
180
+ zero_copy = (
181
+ channel_order == "rgb"
182
+ and pixel == channels
183
+ and channel == 1
184
+ and row >= width * channels
185
+ )
186
+ if zero_copy and isinstance(address, int):
187
+ pointer = ffi.cast("const uint8_t *", address)
188
+ return Pixels(pointer, width, height, channels, row)
189
+ # Any other layout: one C-ordered copy through the buffer protocol.
190
+ try:
191
+ packed = memoryview(cast("Buffer", obj)).tobytes()
192
+ except TypeError:
193
+ raise TypeError(
194
+ "the array does not expose its data through the buffer protocol"
195
+ ) from None
196
+ return _packed(packed, width, height, channels, channel_order)
197
+
198
+
199
+ def _packed(
200
+ data: bytes, width: int, height: int, channels: int, order: ChannelOrder
201
+ ) -> Pixels:
202
+ if order == "bgr":
203
+ if channels < 3:
204
+ raise ValueError("channel_order='bgr' needs 3 or 4 channels")
205
+ swapped = bytearray(data)
206
+ swapped[0::channels], swapped[2::channels] = (
207
+ data[2::channels],
208
+ data[0::channels],
209
+ )
210
+ data = bytes(swapped)
211
+ return Pixels(ffi.from_buffer(data), width, height, channels, 0)
212
+
213
+
214
+ def _shape(shape: tuple[int, ...]) -> tuple[int, int, int]:
215
+ if len(shape) == 2:
216
+ return shape[0], shape[1], 1
217
+ if len(shape) == 3 and shape[2] in (1, 3, 4):
218
+ return shape[0], shape[1], shape[2]
219
+ raise ValueError(
220
+ f"pixels must have shape (height, width) or (height, width, 1|3|4), got {shape}"
221
+ )
222
+
223
+
224
+ _PIL_DIRECT = {"L": 1, "RGB": 3, "RGBA": 4}
225
+ _PIL_UNSUPPORTED = ("I", "I;16", "I;16B", "I;16L", "I;16N", "F")
226
+
227
+
228
+ def pixels_from_pil(img: PILImage) -> Pixels:
229
+ """Read the pixels of a Pillow image.
230
+
231
+ ``L``, ``RGB`` and ``RGBA`` are read as they are. ``1`` becomes ``L``; palette and
232
+ gray-with-alpha images become ``RGBA``, which hashes as ``RGB`` where every pixel is
233
+ opaque; ``CMYK``, ``YCbCr``, ``HSV`` and ``LAB`` become ``RGB``. 16-bit, 32-bit
234
+ integer and float modes are refused: there is no single right way to bring them to
235
+ 8 bits.
236
+ Pillow does not apply the EXIF orientation to the pixels; pass the image through
237
+ ``PIL.ImageOps.exif_transpose`` first if it matters.
238
+ """
239
+ mode = img.mode
240
+ if mode in _PIL_UNSUPPORTED:
241
+ raise TypeError(
242
+ f"Pillow mode {mode!r} is not 8-bit; "
243
+ "convert it (img.convert('L') or 'RGB') first"
244
+ )
245
+ if mode not in _PIL_DIRECT:
246
+ if mode == "1":
247
+ target = "L"
248
+ elif mode in ("P", "PA", "LA", "La", "RGBa"):
249
+ if mode == "La":
250
+ # Pillow converts premultiplied gray-with-alpha only to LA.
251
+ img = img.convert("LA")
252
+ target = "RGBA"
253
+ else:
254
+ target = "RGB"
255
+ img = img.convert(target)
256
+ mode = target
257
+ width, height = img.size
258
+ data = img.tobytes()
259
+ channels = _PIL_DIRECT[mode]
260
+ return Pixels(ffi.from_buffer(data), width, height, channels, 0)
261
+
262
+
263
+ def read_file(path: StrPath) -> bytes:
264
+ """Read an image file for the C library to load from memory.
265
+
266
+ A file that cannot be opened, or is empty, raises the ImageIOError that the C
267
+ library's own open would, with the same message.
268
+ """
269
+ try:
270
+ with open(path, "rb") as f:
271
+ data = f.read()
272
+ except OSError as e:
273
+ detail = f"Cannot open '{os.fsdecode(path)}': {e.strerror}"
274
+ raise make_error(PH_ERR_IO, detail=detail, path=path) from e
275
+ if not data:
276
+ detail = f"Cannot read '{os.fsdecode(path)}': file is empty"
277
+ raise make_error(PH_ERR_IO, detail=detail, path=path)
278
+ return data
libphash/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ # X.Y.Z.N: X.Y.Z is the bundled libphash, N the binding revision on top of it
2
+ # (0 for the first binding of a libphash release, and written out).
3
+ __version__ = "2.0.1.0"
libphash/exceptions.py ADDED
@@ -0,0 +1,157 @@
1
+ """Exceptions raised by libphash.
2
+
3
+ Every failure of the C library becomes a subclass of :class:`PhashError`, chosen by its
4
+ ``ph_error_t`` code. The classes also derive from the built-in exception a Python
5
+ programmer would reach for: an unreadable file is an :class:`OSError` (with ``errno``,
6
+ so ``except FileNotFoundError`` works), a bad argument a :class:`ValueError`, a failed
7
+ allocation a :class:`MemoryError`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from ._native import lib
15
+
16
+ __all__ = [
17
+ "AllocationError",
18
+ "BatchCancelledError",
19
+ "CorruptDataError",
20
+ "DecodeError",
21
+ "DecoderUnavailableError",
22
+ "DigestKindError",
23
+ "EmptyImageError",
24
+ "ImageIOError",
25
+ "ImageTooLargeError",
26
+ "InvalidArgumentError",
27
+ "NoStructureError",
28
+ "PhashError",
29
+ "RequiresColorError",
30
+ "UnsupportedFormatError",
31
+ ]
32
+
33
+
34
+ class PhashError(Exception):
35
+ """Base class of every libphash error.
36
+
37
+ Attributes:
38
+ code: the ``ph_error_t`` value, or 0 for an error raised by the binding itself.
39
+ code_name: the C name of the code (``"PH_ERR_IO"``), or ``""``.
40
+ detail: the C library's description of this failure (for loading: the path and
41
+ the reason), or ``""``.
42
+ """
43
+
44
+ code: int
45
+ code_name: str
46
+ detail: str
47
+
48
+ def __init__(self, message: str, *, code: int = 0, detail: str = "") -> None:
49
+ super().__init__(message)
50
+ self.code = code
51
+ self.code_name = _CODE_NAMES.get(code, "") if code else ""
52
+ self.detail = detail
53
+
54
+ def __reduce__(self) -> tuple[Any, ...]:
55
+ return (_rebuild, (type(self), str(self), self.code, self.detail))
56
+
57
+
58
+ def _rebuild(cls: type[PhashError], message: str, code: int, detail: str) -> PhashError:
59
+ exc = cls.__new__(cls)
60
+ PhashError.__init__(exc, message, code=code, detail=detail)
61
+ return exc
62
+
63
+
64
+ class ImageIOError(PhashError, OSError):
65
+ """The path could not be used as an image source (``PH_ERR_IO``).
66
+
67
+ ``errno`` and ``filename`` are filled in when the cause can be told (a missing file,
68
+ a directory, no permission), so the matching :class:`OSError` subclass catches it:
69
+ ``except FileNotFoundError`` sees a missing image file.
70
+ """
71
+
72
+ def __init__(self, message: str, *, code: int = 0, detail: str = "") -> None:
73
+ PhashError.__init__(self, message, code=code, detail=detail)
74
+
75
+ def __str__(self) -> str:
76
+ # OSError's own would print "[Errno 2] ..." once errno is set.
77
+ return str(self.args[0])
78
+
79
+ def __repr__(self) -> str:
80
+ # The errno-specific subclasses are an implementation detail.
81
+ return f"ImageIOError({str(self)!r})"
82
+
83
+ def __reduce__(self) -> tuple[Any, ...]:
84
+ return (
85
+ _rebuild_io,
86
+ (type(self), str(self), self.code, self.detail, self.errno, self.filename),
87
+ )
88
+
89
+
90
+ def _rebuild_io( # noqa: PLR0913 -- one argument per pickled field
91
+ cls: type[ImageIOError],
92
+ message: str,
93
+ code: int,
94
+ detail: str,
95
+ err: int | None,
96
+ filename: object,
97
+ ) -> ImageIOError:
98
+ exc = cls.__new__(cls)
99
+ ImageIOError.__init__(exc, message, code=code, detail=detail)
100
+ exc.errno = err
101
+ exc.filename = filename
102
+ return exc
103
+
104
+
105
+ class DecodeError(PhashError):
106
+ """The data could not be decoded into an image. Parent of the four reasons below."""
107
+
108
+
109
+ class UnsupportedFormatError(DecodeError):
110
+ """The data is in no image format libphash recognizes."""
111
+
112
+
113
+ class CorruptDataError(DecodeError):
114
+ """The format was recognized, but the data is malformed or truncated."""
115
+
116
+
117
+ class DecoderUnavailableError(DecodeError):
118
+ """The format was recognized, but this build has no decoder for it."""
119
+
120
+
121
+ class ImageTooLargeError(DecodeError):
122
+ """The image is larger than ``Config.max_pixels`` or the library's fixed limits."""
123
+
124
+
125
+ class InvalidArgumentError(PhashError, ValueError):
126
+ """An argument is outside the range the C library accepts."""
127
+
128
+
129
+ class AllocationError(PhashError, MemoryError):
130
+ """A memory allocation failed in the C library."""
131
+
132
+
133
+ class EmptyImageError(PhashError):
134
+ """A hash was requested from a context that holds no image."""
135
+
136
+
137
+ class RequiresColorError(PhashError):
138
+ """A color algorithm was asked for an image loaded in grayscale."""
139
+
140
+
141
+ class NoStructureError(PhashError):
142
+ """A Radial digest has no structure to correlate (a flat image)."""
143
+
144
+
145
+ class BatchCancelledError(PhashError):
146
+ """A batch was stopped before every item was started."""
147
+
148
+
149
+ class DigestKindError(PhashError, TypeError):
150
+ """Two digests were compared by a metric their kind does not support."""
151
+
152
+
153
+ _CODE_NAMES: dict[int, str] = {
154
+ int(getattr(lib, name)): name
155
+ for name in dir(lib)
156
+ if name.startswith("PH_ERR_") and not name.endswith("_")
157
+ }
libphash/py.typed ADDED
File without changes
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-libphash
3
+ Version: 2.0.1.0
4
+ Summary: Perceptual image hashing: Python binding for the libphash C library
5
+ Author-email: gudoshnikovn <gudoshnikovna@gmail.com>
6
+ License-Expression: MIT AND IJG AND Zlib AND libpng-2.0 AND BSD-3-Clause AND (MIT OR Unlicense)
7
+ Project-URL: Homepage, https://github.com/gudoshnikovn/python-libphash
8
+ Project-URL: Source, https://github.com/gudoshnikovn/python-libphash
9
+ Project-URL: Issues, https://github.com/gudoshnikovn/python-libphash/issues
10
+ Project-URL: Changelog, https://github.com/gudoshnikovn/python-libphash/blob/main/CHANGELOG.md
11
+ Project-URL: libphash, https://gudoshnikovn.github.io/libphash/
12
+ Keywords: perceptual hash,phash,image hashing,duplicate images,dhash
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: C
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Programming Language :: Python :: 3.15
27
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
28
+ Classifier: Programming Language :: Python :: Implementation :: CPython
29
+ Classifier: Topic :: Multimedia :: Graphics
30
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
31
+ Classifier: Typing :: Typed
32
+ Requires-Python: >=3.10
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ License-File: native/libphash/THIRD-PARTY-NOTICES.md
36
+ License-File: native/libphash/vendor/libjpeg-turbo/LICENSE.md
37
+ License-File: native/libphash/vendor/libjpeg-turbo/README.ijg
38
+ License-File: native/libphash/vendor/libpng/LICENSE
39
+ License-File: native/libphash/vendor/libwebp/COPYING
40
+ License-File: native/libphash/vendor/libwebp/PATENTS
41
+ License-File: native/libphash/vendor/zlib-ng/LICENSE.md
42
+ Requires-Dist: cffi>=2.0
43
+ Provides-Extra: dev
44
+ Requires-Dist: pytest; extra == "dev"
45
+ Requires-Dist: pytest-randomly; extra == "dev"
46
+ Requires-Dist: pytest-timeout; extra == "dev"
47
+ Requires-Dist: hypothesis; extra == "dev"
48
+ Requires-Dist: psutil; extra == "dev"
49
+ Requires-Dist: coverage; extra == "dev"
50
+ Requires-Dist: mypy; extra == "dev"
51
+ Requires-Dist: basedpyright; extra == "dev"
52
+ Requires-Dist: ruff; extra == "dev"
53
+ Requires-Dist: typos; extra == "dev"
54
+ Requires-Dist: setuptools; extra == "dev"
55
+ Requires-Dist: types-setuptools; extra == "dev"
56
+ Requires-Dist: types-cffi; extra == "dev"
57
+ Requires-Dist: types-psutil; extra == "dev"
58
+ Requires-Dist: typing_extensions; extra == "dev"
59
+ Requires-Dist: numpy; extra == "dev"
60
+ Requires-Dist: Pillow; extra == "dev"
61
+ Requires-Dist: opencv-python-headless; extra == "dev"
62
+ Requires-Dist: mutmut; sys_platform != "win32" and extra == "dev"
63
+ Requires-Dist: lief; extra == "dev"
64
+ Dynamic: license-file
65
+
66
+ # python-libphash
67
+
68
+ Perceptual image hashing in Python, with the [libphash](https://github.com/gudoshnikovn/libphash)
69
+ C library inside.
70
+
71
+ [![CI](https://github.com/gudoshnikovn/python-libphash/actions/workflows/ci.yml/badge.svg)](https://github.com/gudoshnikovn/python-libphash/actions/workflows/ci.yml)
72
+ [![PyPI](https://img.shields.io/pypi/v/python-libphash.svg)](https://pypi.org/project/python-libphash/)
73
+ [![Python](https://img.shields.io/pypi/pyversions/python-libphash.svg)](https://pypi.org/project/python-libphash/)
74
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/gudoshnikovn/python-libphash/blob/main/LICENSE)
75
+
76
+ A perceptual hash changes little when an image is resized, recompressed or slightly
77
+ edited, so comparing hashes finds the copies of a picture among many images; an
78
+ unrelated picture lands about half the bits away. The package computes nine algorithms
79
+ in C, decodes JPEG, PNG and WebP with libraries linked into the wheel, and gives the same
80
+ hash for the same file on every platform, bit for bit libphash's.
81
+
82
+ **[Documentation](https://gudoshnikovn.github.io/python-libphash/)** · the theory
83
+ (algorithms, comparing, choosing a threshold) is on
84
+ [libphash's site](https://gudoshnikovn.github.io/libphash/).
85
+
86
+ ```bash
87
+ pip install python-libphash
88
+ ```
89
+
90
+ Wheels for Linux (x86-64, arm64), macOS (arm64) and Windows (x86-64), CPython 3.10 and
91
+ later, free-threaded builds included; elsewhere pip builds from source with a C compiler.
92
+
93
+ ```python
94
+ import libphash as ph
95
+ from libphash import Algorithm
96
+
97
+ a = ph.hash("photo.jpg") # pHash, a 64-bit int
98
+ b = ph.hash("photo_copy.jpg")
99
+ print(ph.hamming_distance(a, b)) # 0..64 bits differ: 0 for a copy
100
+
101
+ r = ph.hash("photo.jpg", Algorithm.RADIAL) # a Digest
102
+ print(r.similarity(ph.hash("photo_rotated.jpg", Algorithm.RADIAL)))
103
+
104
+ results = ph.hash_files(["photo.jpg", "photo_copy.jpg"], threads=0) # all CPUs
105
+ ```
106
+
107
+ `ph.hash()` takes a path, encoded bytes, a file object, a numpy array of pixels or a
108
+ Pillow image; `ImageContext` computes several hashes of one image and reuses its buffers
109
+ from image to image; `Config` holds every setting of libphash and checks it.
110
+
111
+ From a shell, `libphash hash`, `compare` and `dupes` do the same for files
112
+ (`python -m libphash` where the command is not on `PATH`):
113
+
114
+ ```bash
115
+ libphash dupes --max-bits 10 ~/Pictures
116
+ ```
117
+
118
+ ## Scope
119
+
120
+ Hashes are deterministic and unkeyed: they find the copies of a picture in a collection
121
+ you control. They are not for moderation, blocklists or checking untrusted input, since
122
+ anyone who can choose the images can make two pictures hash alike or keep a copy from
123
+ matching. Decoding files from anywhere is the expected use, and a crash on one is a
124
+ vulnerability: see [SECURITY.md](https://github.com/gudoshnikovn/python-libphash/blob/main/SECURITY.md).
125
+
126
+ ## Versions
127
+
128
+ The version is `X.Y.Z.N`: `X.Y.Z` is the bundled libphash, `N` the revision of the
129
+ binding. Hash values depend on `X.Y.Z` and the settings only, so
130
+ `pip install "python-libphash==2.0.1.*"` keeps a stored collection comparable while
131
+ taking the binding's fixes. Upgrading from 1.x changes hash values and the API: read
132
+ [MIGRATION.md](https://github.com/gudoshnikovn/python-libphash/blob/main/MIGRATION.md).
133
+
134
+ ## License
135
+
136
+ MIT, see [LICENSE](https://github.com/gudoshnikovn/python-libphash/blob/main/LICENSE). The
137
+ wheels include libphash and the libraries it bundles (libjpeg-turbo, libpng, libwebp,
138
+ zlib-ng, stb_image), each under its own license, carried in the wheel.
@@ -0,0 +1,29 @@
1
+ libphash/__init__.py,sha256=gjy7yG4OfjBDpB4p7GtzHHoCJvr85bbbJOQ0p3xaENA,5775
2
+ libphash/__main__.py,sha256=sLikFD6ofBKa5swu8fWWCxDH_FlzidWH3JJcmXH1CnA,145
3
+ libphash/_algorithms.py,sha256=ZAk8KqF_d1XyDHaMpCMQJSeSu6XYnsPHbu2c_6eTdBQ,2689
4
+ libphash/_batch.py,sha256=mDxyMOpVsKeLikotxGQNuU24LsaC1UllSlbwsJQlPYs,13263
5
+ libphash/_cli.py,sha256=Au033w2Q2ww50i-294aoC7mSR4SfUiobLOgaL6KYHZ8,18598
6
+ libphash/_config.py,sha256=s8hy6sbVZ2_pW-aOJsMFN1RkGWL7gmzrc4RHBIut0WI,8213
7
+ libphash/_context.py,sha256=QrGMeI9VZUvqmfTm0OQpbWqIZfjbN5rQbPq5iQceLm8,14309
8
+ libphash/_digest.py,sha256=pBRXOwDXPcvP-L9qR2rDqrUwuoonRxnQgqksB9Ucbjs,11201
9
+ libphash/_errors.py,sha256=nnOrzpFt4sW8I3-tO60MKWTIfSYh8DvgmRgOxuEAtec,3695
10
+ libphash/_native.pyd,sha256=_2mf2F4YV12ONgcNnmHbz9W-w-hi34MKeCC3hD_BSUw,1292288
11
+ libphash/_native.pyi,sha256=DxfxZtvVnG2FiRZbYqGzL_USBEaB0NRNuGmsgUoAshY,31335
12
+ libphash/_sources.py,sha256=fiR6PMkQGP9KP4GrbjX-yn72B4Zz9siSNf8Z3EdzB1w,9888
13
+ libphash/_version.py,sha256=hzV08Wy1L0XhmKoiIdHtGxsgDAIydpymWju7paJUGlA,173
14
+ libphash/exceptions.py,sha256=zIxDbRIDhI1SnYlxIcwZdMTkppKoy1GtQT4lD0zKmQM,4881
15
+ libphash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ python_libphash-2.0.1.0.dist-info/licenses/LICENSE,sha256=y0PvbnMHXl-CFTfDpc55kAgVCUT22vsWFpdkOp11-N8,1090
17
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/THIRD-PARTY-NOTICES.md,sha256=e95tvS69Y0JFF2BnpN91ymIuOERf3u0qe4kR-t4hZjA,8236
18
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/vendor/libjpeg-turbo/LICENSE.md,sha256=z_hwHcmZ45tQROzo8i7WWzoQ1qjb3cqP-4AtJFgBgds,6224
19
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/vendor/libjpeg-turbo/README.ijg,sha256=2xagQSgXGHnGBwjRcbiNlzRaLdIPm_wXNoCkSXxz9wQ,13059
20
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/vendor/libpng/LICENSE,sha256=RSOQQzug-Iqj4rEixkd0G3KgwRfNbtejKbSXha7LVRE,5479
21
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/vendor/libwebp/COPYING,sha256=F8ApqQKtRaGZ3-jjocOTBaso0wKwcDNgxKJzUaRnPcs,1526
22
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/vendor/libwebp/PATENTS,sha256=1Xi3pHHUhAahbGTjxqhoXmYpLzxnCSFMScpVsvIu180,1459
23
+ python_libphash-2.0.1.0.dist-info/licenses/native/libphash/vendor/zlib-ng/LICENSE.md,sha256=bJ8Nl1tBr6o00i9Vu4mGzmnly3rTJ8srKIIM1CXt9e4,867
24
+ python_libphash-2.0.1.0.dist-info/sboms/python-libphash.cdx.json,sha256=H9ZrHM4lLPhzasvsTUvVUi1lpKovJ33FGKS6S3Jn07c,3814
25
+ python_libphash-2.0.1.0.dist-info/METADATA,sha256=m7B84MRFIVQvwCMTq-faVuDZlmOBSK8V7_coXGFyzLQ,6765
26
+ python_libphash-2.0.1.0.dist-info/WHEEL,sha256=_nx_6fOfFB2ir5m3GgtWLIoU4BeBcd0Dd-_9POcQKIM,102
27
+ python_libphash-2.0.1.0.dist-info/entry_points.txt,sha256=JBVFrsnjo0jM6NzdRnl_goKwNQAg5ciaUTy4e8nfFHk,48
28
+ python_libphash-2.0.1.0.dist-info/top_level.txt,sha256=_Iys95-wvePR99MGJ1ikgjscpcotnUJ2m3JzmLvCG7w,9
29
+ python_libphash-2.0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp315-cp315t-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ libphash = libphash._cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gudoshnikovn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.