Changelog¶
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog. The
version is X.Y.Z.N: X.Y.Z is the bundled libphash, N the revision of the binding
(CONTRIBUTING.md, "Versions"). What changes in libphash itself is in
its changelog; this
one says what a Python user observes.
Unreleased¶
Nothing yet.
2.0.2.0 - 2026-10-15¶
Bundles libphash 2.0.2. The API and the hash values are those of 2.0.1.1; only the files below are answered differently.
Security¶
- A truncated TGA or HDR file raises
CorruptDataErrorinstead of being hashed from uninitialized memory. The stb_image decoder of libphash 2.0.1, in every wheel, decoded an uncompressed TGA or a narrow HDR whose pixel data ended early as a whole image, the missing pixels being whatever memory held, so the same file hashed differently from load to load. libphash 2.0.2 rejects such a file (GHSA-xjg6-832m-2xwg).
Fixed¶
- A FIFO with no writer raises
ImageIOErrorat once. On Linux and macOS,hash(),ImageContext.load_file()and thelibphashcommand waited for a writer that never came, and one such path among those ofhash_files()held up its worker and the rest of the batch. It is "not a regular file", as a device is.
2.0.1.1 - 2026-10-14¶
The same library, API and hash values as 2.0.1.0.
Fixed¶
- The GitHub Release has a build provenance attestation for each wheel, the sdist and the
SBOM, checked with
gh attestation verify; the GitHub Release of 2.0.1.0 has the same files without one. On PyPI, every file of both versions has its PEP 740 attestation.
2.0.1.0 - 2026-10-14¶
The first release on libphash 2 (2.0.1; 1.4.1 bundles libphash 1.10.4), with a new
Python API. Hash values change, and code written for 1.x does not run unchanged:
read MIGRATION.md before upgrading.
BREAKING CHANGES¶
- Stored hashes must be recomputed. libphash 2.0 changes the values of every
algorithm on some inputs: aHash, pHash and BMH threshold differently, mHash, ColorHash,
ColorMoments and Radial are rewritten, JPEG is decoded with the accurate IDCT, photos
are turned upright by their EXIF orientation, and transparent images are composited
onto gray. Nothing reports the difference: a 1.x hash compared with a 2.x one simply
stops matching.
MIGRATION.mdlists each change and the two thatConfigcan undo (auto_orient=False,alpha_mode="ignore"). - A new API replaces the 1.x one.
hash()hashes one image from any source; anImageContextis created empty, loads image after image withload_*()methods and computes hashes withhash(),hashes()or a method per algorithm; every setting is a field of an immutableConfig.HashMethod,get_hash(),compare_images(),ph_can_use_libjpeg(),ph_can_use_libpng(), theset_*()methods and the constructor argumentspath,bytes_dataandload_grayscaleare gone;MIGRATION.mdmaps each to its replacement. - mHash and ColorHash return a
Digest(72 and 108 bytes), not anint. The context's hash properties are methods (ctx.phash()), andcolor_moments_hashandradial_hasharecolor_moments()andradial(). Digestcarries its kind. It is created asDigest(data, kind)instead ofDigest(data, size);len(digest)replacessize. Its comparisons arehamming_distance(),l2_distance(),histogram_intersection(),radial_similarity()andsimilarity(), and each refuses a digest of a kind it does not apply to withDigestKindError.distance_hamming()anddistance_l2()are gone.- Failures raise specific exceptions. A missing or unreadable file raises
ImageIOError, anOSErrorwitherrno(FileNotFoundErrorcatches a missing file), where 1.x raisedDecodeError;except DecodeErrorno longer catches it. A failed decode raises one of the four subclasses ofDecodeError:UnsupportedFormatError,CorruptDataError,DecoderUnavailableError,ImageTooLargeError.InvalidArgumentErroris aValueError,AllocationErroraMemoryError. A hash of an empty context raisesEmptyImageError, a color hash of a grayscale imageRequiresColorError. - Settings are checked. A
Configwith a value outside the range libphash accepts raisesInvalidArgumentErrornaming the field, where a 1.x setter ignored the value or replaced it with a default. - Images over 256 Mi pixels, or over 1,000,000 pixels on a side, are refused with
ImageTooLargeErrorbefore they are decoded.Config(max_pixels=…)moves the first limit; the second is fixed. - Radial's
gammais the exponent itself, default 1.0; 1.x raised pixels to1 / gamma, default 2.2. - Python 3.10 or later is required (1.4.1: 3.8, with wheels from 3.9).
- x86-64 wheels need a CPU with SSE4.2 and POPCNT (x86-64-v2), the baseline libphash is built and tested for; the 1.4.1 wheels ran on any x86-64 CPU.
- The version has four numbers,
X.Y.Z.N, whereX.Y.Zis the bundled libphash.
Added¶
hash(source, algorithm, config=)accepts a path, encoded bytes, a binary file object, an 8-bit array of pixels (numpy or anything with__array_interface__; RGB, BGR, gray, with or without alpha, any strides) and a Pillow image. An array whose only gaps are between rows is hashed without a copy.Algorithm, an enum of the nine algorithms, accepted wherever an algorithm is; each algorithm's libphash name ("radial") is accepted too.ImageContext.hashes()computes several hashes of one image, the 64-bit ones in a single pass over the image.hash_files()andhash_buffers()hash many images on libphash's thread pool, with a progress callback that can stop the batch, aBatchResultper item, and Ctrl+C that stops the batch and raisesKeyboardInterrupt.Config: every setting of libphash 2.0.1, among themauto_orient,alpha_mode,max_pixels,decode_scale(JPEG decoded at ½, ¼ or ⅛), the mHash and Radial parameters andwhash_remove_max_haar_ll.similarity()for two 64-bit hashes andDigest.similarity(), each with the measure that belongs to the algorithm;RADIAL_PCC_THRESHOLD.- Text forms that keep the kind:
Digest.hex(),Digest.from_hex()("bits:3fa0…", interchangeable with libphash's),hash_to_hex(),hash_from_hex();Digest.from_uint64(). libphash_version(),build_info(),can_decode(),digest_info().- A documentation site: https://gudoshnikovn.github.io/python-libphash/. Guides to installing, loading, configuring, errors, batches, threads and processes, storage and search, and performance, the API reference from the docstrings, and the examples; the theory links to libphash's site. The code on every page runs in the test suite.
- A command,
libphash(andpython -m libphash):hashprints the hashes of files and directories,comparecompares two images by the measures of each algorithm,dupesgroups near-duplicates within a given number of bits,infoprints the versions and the build for bug reports;--jsonfor scripts. examples/: runnable scripts, each checked by the tests against the output its docstring shows: loading from every source, configuration, duplicates in a batch, SQLite storage, nearest hashes with numpy, a BK-tree, concurrency, and choosing a threshold. Four are twins of libphash's C examples and print the same output.- Exceptions carry the C error (
code,code_name) and, for a failed load, libphash's own message (detail); they pickle. - Type information: the package is typed (
py.typed) and checked with mypy and basedpyright in strict mode, and exports the types its signatures use (Source,StrPath,PixelArray,AlgorithmLike, theConfigliterals, the protocols). - Thread safety: a context may be shared between threads, its calls are serialized;
Config,Digest, results and errors are immutable and pickle, so they cross process boundaries. - A software bill of materials in every wheel (
.dist-info/sboms/, CycloneDX): libphash and each library linked into it, with its version and license. The license of each bundled library is in the wheel and the sdist, andLicense-Expressioncovers them all. - Releases are published with PEP 740 attestations on PyPI and build provenance on the
GitHub Release (
SECURITY.md, "Verifying a release"); the sdist is reproducible. - Wheels for Linux arm64 and Windows x86-64, and for the free-threaded builds of
CPython 3.14 and later. A wheel per platform for CPython with the GIL, tagged
abi3: it installs on every CPython from 3.10 on, a later one included. Paths with any characters, Cyrillic, CJK and emoji included, load on Windows.
Changed¶
- libphash is built by its own CMake project and linked statically: the build of
the wheel is the build libphash tests, with libjpeg-turbo, libpng, libwebp and zlib-ng
vendored. The extension module exports one symbol,
PyInit__native, so the bundled decoders cannot clash with another copy of the same libraries in the process (Pillow, OpenCV). - The same image gives the same hash on every platform: arm64 and x86-64, Linux, macOS and Windows.
- PNG is decoded with libpng on every platform; the 1.4.1 wheels for Linux used spng 0.7.4, which has had no release since May 2023.
- zlib is linked into the wheel (zlib-ng); the 1.4.1 wheels for Linux used the system's zlib.
- cffi 2.0 or later is required (1.4.1: 1.15).
- Bundled libraries: libphash 2.0.1, libjpeg-turbo 3.2.0, libpng 1.6.59, libwebp 1.6.0, zlib-ng 2.3.3, stb_image 2.30.
Fixed¶
__version__is the package's version (1.4.1 reported1.4.0).- A truncated JPEG or PNG, and a PNG whose critical chunk has a bad checksum, raise
CorruptDataErrorwith every decoder.
Removed¶
- The
benchmarks/directory of the source tree. - Wheels for 32-bit x86 Linux and for musl Linux, which libphash does not test. pip builds the sdist there; it needs a C compiler.
Security¶
- Bundled libpng 1.6.59. The 1.4.1 wheel for macOS carries libpng 1.6.56, affected by
CVE-2026-46675, a use-after-free in
png_read_end()after an incompletezTXt,iTXtoriCCPdecompression, and by a double free after an allocation failure; libphash's PNG decoder does not callpng_read_end()on that path. The 1.4.1 wheels for Linux decode PNG with spng 0.7.4, which is no longer maintained. - Images are refused before they are decoded when they exceed
Config.max_pixelsor the per-side limit: 1.x decoded an image of any declared size, so a small file could make it allocate gigabytes (a decompression bomb). - Pixel counts are computed without integer overflow on large images.
- A
Digestholds at mostPH_DIGEST_MAX_BYTESbytes and its size is its length: in 1.x,Digest(data, size)took the size from the caller, and a comparison readsizebytes from a 64-byte buffer. - On the 32-bit x86 wheels of 1.x, a large BMH block size (
set_block_params()) overflowed the size of a scratch buffer, and the hash wrote past it on the heap. The block size is 2 to 32 (Config.bmh_block_size), and sizes are computed with overflow checks.
1.x¶
The 1.x releases were numbered independently of libphash. Their notes are in the GitHub releases of the repository.
| Version | libphash |
|---|---|
| 1.4.1 | 1.10.4 |
| 1.4.0 | 1.10.1 |
| 1.3.0 | 1.9.0 |
| 1.2.0 | 1.6.1 |
| 1.1.0 | 1.3.0 |
| 1.0.3 | 1.2.0 |