Migration guide: 1.x → 2.0¶
This guide is written for a program built against libphash 1.10.4. Upgrading to 2.0
changes three things: several functions changed signature and two error constants were
removed, so some code no longer compiles; some calls that compile unchanged now behave
differently; and the same input can produce a different hash. The sections below follow
that order, then cover building and linking. CHANGELOG.md's ## [2.0.0] section has
the full list of changes; this document is the action list.
Before you upgrade: read this first¶
If you store hash values anywhere (a database, an index, a dedup table), the values you already have are about to go stale for reasons that produce no error and no warning. The single biggest cause is auto-orientation (below), because it silently affects most photos ever taken on a phone or camera. Recall in deduplication/search will quietly degrade after upgrading unless you plan for it.
Strategy for an existing hash database¶
Pick one, in order of how much work it costs you:
- Recompute everything. Simplest and correct. If your corpus is small enough or you can afford a batch job, just do this and stop reading this subsection.
- Recompute lazily. Rehash an item the next time it's read/compared; tolerate stale hashes for items that are never touched again.
- Store the algorithm version next to every hash value, and recommended even if
you do (1) or (2): a plain
uint64_t/ph_digest_ton its own carries no way to tell a 1.x hash from a 2.0 one apart after the fact. Add a version column/field (e.g.PH_VERSION_NUMBERat the time the hash was computed, or just a "2" you bump yourself) so a future upgrade doesn't repeat this problem blind. This is the one piece of housekeeping most callers don't already have, and the main practical takeaway of this guide.
/* Minimal example of (3): pair every stored hash with the version that produced it. */
struct stored_hash {
uint64_t phash;
uint32_t computed_with_version; /* = PH_VERSION_NUMBER at insert time */
};
Restoring 1.x hash values (where possible)¶
Auto-orientation and alpha compositing are the two breaking changes you can opt out of. Every other hash value change below (aHash, pHash, BMH, color hash, mHash, color moments, radial, JPEG decoding) has no "old mode" switch — recompute is the only path, per each item's own entry.
ph_context_t *ctx;
ph_create(&ctx);
ph_context_set_auto_orient(ctx, 0); /* hash the stored pixels, as 1.x did */
ph_context_set_alpha_mode(ctx, PH_ALPHA_IGNORE); /* drop alpha, as 1.x did */
Read the EXIF-orientation and alpha entries below before doing this: both make the hash describe something a viewer does not see — the undisplayed sensor buffer, the color under transparent pixels — which is usually not what you actually want long-term. They are offered as a bridge to keep old values valid while you plan a rehash, not as the recommended steady state.
Code that no longer compiles¶
ph_compute_mhash() and ph_compute_color_hash() return a ph_digest_t¶
Both algorithms are rewritten (see "Rewritten algorithms" below), and neither result
fits a uint64_t: mHash is 576 bits, ColorHash a 108-bin histogram.
/* Before (1.x) */
uint64_t m, c;
ph_compute_mhash(ctx, &m);
ph_compute_color_hash(ctx, &c);
/* After (2.0) */
ph_digest_t m, c;
ph_compute_mhash(ctx, &m); /* compare with ph_hamming_distance_digest() */
ph_compute_color_hash(ctx, &c); /* compare with ph_histogram_intersection() */
ph_context_set_radial_params() takes a third argument, sigma¶
sigma is the Gaussian blur applied before the projections (default 3.5). The first
argument keeps its name but not its meaning: in 1.x the projection count was also the
digest width (default 40); in 2.0 it is the number of angles only (default 180), and the
digest is always the first 40 coefficients of a DCT over them.
/* Before (1.x) */
ph_context_set_radial_params(ctx, 40, 128);
/* After (2.0): the defaults, spelled out */
ph_context_set_radial_params(ctx, 180, 128, 3.5f);
A 1.x value of projections does not reproduce 1.x hashes in 2.0; the defaults are the
algorithm's source's.
PH_ERR_DECODE_FAILED is removed¶
In 1.x it was the single code for every load failure. 2.0 reports the specific cause instead, so a check against the old name no longer compiles — which is the point: replace it with the codes you need.
| 1.x code (removed) | What the failure actually was | 2.0 code |
|---|---|---|
PH_ERR_DECODE_FAILED |
Format not recognized at all | PH_ERR_UNSUPPORTED_FORMAT |
PH_ERR_DECODE_FAILED |
Format recognized, bytes corrupt/truncated | PH_ERR_CORRUPT_DATA |
PH_ERR_DECODE_FAILED |
File/path could not be read at all | PH_ERR_IO |
PH_ERR_DECODE_FAILED |
Format recognized, no decoder compiled in (e.g. WebP without PH_USE_WEBP) |
PH_ERR_DECODER_UNAVAILABLE |
PH_ERR_DECODE_FAILED |
Decoder ran out of memory | PH_ERR_ALLOCATION_FAILED |
2.0 also rejects images above a size limit with PH_ERR_IMAGE_TOO_LARGE; 1.x had no
such limit (see "Large images are rejected by default" below).
/* Before (1.x) */
ph_error_t err = ph_load_from_file(ctx, path);
if (err == PH_ERR_DECODE_FAILED) {
log_skip(path);
}
/* After (2.0) */
ph_error_t err = ph_load_from_file(ctx, path);
switch (err) {
case PH_ERR_UNSUPPORTED_FORMAT:
case PH_ERR_CORRUPT_DATA:
case PH_ERR_IMAGE_TOO_LARGE:
case PH_ERR_IO:
case PH_ERR_DECODER_UNAVAILABLE:
log_skip(path);
break;
default:
break;
}
ph_get_last_error_message() adds the decoder's own reason for a failed load.
PH_ERR_NOT_IMPLEMENTED is removed¶
Nothing in 1.x returned it, so a check against it could never fire. Its value -4 is
reserved and never reused. Delete any branch that tests for it. A feature missing from a
build is PH_ERR_DECODER_UNAVAILABLE for a decoder; a build without threads runs a
batch sequentially.
ph_can_use_libjpeg()/ph_can_use_libpng() are ph_can_use_jpeg()/ph_can_use_png()¶
All three capability checks, with ph_can_use_webp(), are named after the format.
Return values are unchanged; rename the calls. ph_get_build_info() names the
library behind each format.
ph_digest_t is 136 bytes and carries a kind tag¶
PH_DIGEST_MAX_BYTES is 128 (1.x: 64), and the byte after size is a
ph_digest_kind_t kind tag (1.x: padding). Source code that treats the struct through
its fields recompiles unchanged; anything that hardcodes the 1.x layout — a 72-byte
struct, no kind byte — must be rebuilt against the 2.0 header: an FFI binding,
serialization code, a stored raw struct.
Code that compiles but behaves differently¶
Config setters return ph_error_t and reject invalid input¶
In 1.x the setters returned void, and an out-of-range value was ignored (or, for the
gray weights, replaced by the defaults). In 2.0 a valid argument returns PH_SUCCESS;
anything else returns PH_ERR_INVALID_ARGUMENT and leaves the configuration
completely unchanged. Existing call sites still compile — the setters are
deliberately not warn_unused_result — but check the result wherever the arguments come
from outside your code.
/* Before (1.x): no return value */
ph_context_set_phash_params(ctx, dct_size, reduction_size);
/* After (2.0) */
if (ph_context_set_phash_params(ctx, dct_size, reduction_size) != PH_SUCCESS) {
/* dct_size/reduction_size out of bounds; the previous setting is still active */
handle_bad_config();
}
Bounds: gamma finite in (0.001, 1000]; gray weights each ≥ 0 with a sum in
(0, INT_MAX/255] (a zero sum is an error, not a request for the defaults);
reduction_size 4..8 and dct_size reduction_size..32 (a 2×2 or 3×3 block gives most
unrelated images the same pHash); radial projections 40..4096 and samples 2..4096
(the digest has converged far below both; above them a call only costs more CPU),
sigma in (0, 64/3]; block_size 2..32 (1.x
accepted any positive value and truncated the BMH digest to 64 bytes above 22×22);
whash_mode a declared enumerator only.
ph_context_set_gamma(): exponent gamma, default 1.0¶
1.x raised pixels to 1/gamma with a default of 2.2. 2.0 follows pHash: pixels are
normalized by the buffer's maximum, raised to gamma, and rescaled, with a default of
1.0 — an identity. Gamma affects Radial only, in both versions. If you set gamma
explicitly, the same number now means the inverse; review it or drop the call.
Hashing a context with no image returns PH_ERR_EMPTY_IMAGE¶
In 1.x every ph_compute_* function returned PH_ERR_INVALID_ARGUMENT for a context
with nothing loaded. 2.0 returns PH_ERR_EMPTY_IMAGE (-5) for a fresh context and for
one whose last ph_load_from_file()/ph_load_from_memory() failed.
PH_ERR_INVALID_ARGUMENT means only what it says: a NULL pointer or an out-of-range
value. Pointer arguments are checked first, so a NULL output pointer is
PH_ERR_INVALID_ARGUMENT whether or not an image is loaded.
/* Before (1.x): "forgot to load" and "passed NULL" were the same code */
if (ph_compute_phash(ctx, &hash) == PH_ERR_INVALID_ARGUMENT) { ... }
/* After (2.0) */
switch (ph_compute_phash(ctx, &hash)) {
case PH_ERR_EMPTY_IMAGE: /* the load before this failed, or never happened */
break;
case PH_ERR_INVALID_ARGUMENT: /* a bug in the call itself */
break;
default:
break;
}
Large images are rejected by default¶
1.x decoded any size. 2.0 refuses an image whose width × height exceeds max_pixels
(default 256 Mi = 268,435,456 pixels) before allocating it, returning
PH_ERR_IMAGE_TOO_LARGE. Raise the limit with ph_context_set_max_pixels(); 0 removes
your own limit, but an implementation ceiling of INT_MAX (2,147,483,647) pixels always
applies.
Independently of max_pixels, a single dimension may not exceed 1,000,000 pixels —
setting max_pixels higher or to 0 does not lift it. In practice this only affects
synthetic input (an aspect ratio like 268,435,456×1 sits exactly on the default area limit
but would make the decoder allocate an ~800 MB single row); real photographs are nowhere
near it.
Color algorithms refuse grayscale images¶
ph_compute_color_hash() and ph_compute_color_moments_hash() return
PH_ERR_REQUIRES_COLOR when the loaded image has fewer than 3 channels. 1.x read r, g
and b out of the one channel and returned a meaningless but successful result.
ph_context_set_load_grayscale(ctx, 0); /* default; load in color before a color hash */
if (ph_load_from_file(ctx, path) == PH_SUCCESS) {
ph_digest_t d;
if (ph_compute_color_hash(ctx, &d) == PH_ERR_REQUIRES_COLOR) {
/* the image has < 3 channels -- skip it, this call cannot succeed on it */
}
}
Digest comparisons check size and kind¶
- A digest whose
sizeexceedsPH_DIGEST_MAX_BYTES(128) is rejected with-1. 1.x readsizebytes as given, past the end ofdatafor anything above 64. - A
sizeof0is-1from every comparison. 1.x returned0, i.e. "identical", fromph_hamming_distance_digest()andph_l2_distance()for two empty digests. - Each comparison refuses a digest tagged with a kind it is not for:
ph_hamming_distance_digest()returns-1for a radial or color-moments digest, andph_l2_distance()returns-1for a BMH digest.PH_DIGEST_KIND_UNSPECIFIEDis0, so a hand-filled struct that never setskindis accepted by every comparison (no protection, but no new failure either).
ph_digest_t d = {0};
d.size = 200; /* invalid: > PH_DIGEST_MAX_BYTES */
ph_hamming_distance_digest(&d, &other); /* -1; 1.x read past the struct */
Non-regular files are rejected¶
ph_load_from_file() returns PH_ERR_IO for a FIFO, a character device or /dev/stdin;
1.x read them. Read the stream into a buffer yourself and call ph_load_from_memory().
Regular files are unaffected.
Transparent images are hashed as they look¶
1.x dropped the alpha channel and hashed the color stored under it, which is invisible
and differs between encoders: two copies of the same icon, one with black and one with
white under its transparent pixels, hashed as far apart as two unrelated images. 2.0 composites every
image with alpha (an alpha channel or a PNG tRNS chunk, or RGBA passed to
ph_load_from_pixels()) onto mid-gray at load time, so such copies hash identically.
Every stored hash of an image with transparency changes; opaque images are unaffected.
ph_context_set_alpha_mode() picks a white or black background instead, or
PH_ALPHA_IGNORE for the 1.x behavior. ph_context_get_dimensions() reports 3
channels (1 when loaded as grayscale) for such an image: alpha is never stored.
EXIF auto-orientation is on by default¶
Images carrying an Orientation tag other than 1 (most phone/camera photos) are
rotated/mirrored before hashing, so the hash describes what a viewer displays rather than
the stored pixels, which is what 1.x hashed. See "Restoring 1.x hash values" above for the
opt-out, and "Strategy for an existing hash database" for what to do instead of opting
out.
Hash values¶
Which stored values change¶
Measured on 807 JPEG and PNG files (photographs, wallpapers, textures, icons), each hashed by 1.10.4 and by 2.0, both built with the Makefile — stb_image decodes JPEG and PNG in both, so the JPEG decoder change below is not in these numbers — and with auto-orientation off:
| Algorithm | Values that change | Why | Opt out |
|---|---|---|---|
| aHash | 572 of 807 (71 %) | exact area-average reduction, >= against the exact mean |
no |
| pHash | 490 of 807 (61 %) | median threshold with a margin | no |
| BMH | 639 of 807 (79 %) | median threshold, not mean | no |
| dHash | 170 of 807 (21 %), all PNGs with transparency | alpha compositing | PH_ALPHA_IGNORE restores all but 2 |
| wHash | 129 of 807 (16 %), all PNGs with transparency | alpha compositing | PH_ALPHA_IGNORE restores all but 2 |
| mHash, ColorHash, ColorMoments, Radial | all | rewritten; the digests are a different size or kind | no |
On top of these, in every algorithm: a photo with an EXIF/WebP rotation tag hashes as
displayed (opt out with ph_context_set_auto_orient(ctx, 0)), and with the native JPEG
decoder every JPEG hash can move by a few bits (below). A 1.x build that fused
multiply-adds — Clang's default on arm64 — also differs in pHash on another 20 of the 807.
Rewritten algorithms: no compatible mode, recompute is the only option¶
Each of these changed enough that a stored 1.x value cannot be compared against a
2.0 value at all — treat every stored digest from these five algorithms as invalid and
plan a rehash of anything that used them. Full rationale for each is in CHANGELOG.md;
this is the "what do I call now" summary.
ph_compute_color_hash()— a 108-byte color histogram (PH_DIGEST_KIND_HISTOGRAM), not 1.x's 42-bit value; compare withph_histogram_intersection(), notph_hamming_distance_digest().
ph_digest_t a, b;
ph_compute_color_hash(ctx_a, &a);
ph_compute_color_hash(ctx_b, &b);
double score;
ph_histogram_intersection(&a, &b, &score); /* not ph_hamming_distance_digest() */
-
ph_compute_mhash()— a Marr–Hildreth hash, 72-byte digest (576 bits), not 1.x's 64-bit discrete-Laplacian value.ph_context_set_mhash_params()exposes the kernel'salpha/leveland the normalization size. -
ph_compute_color_moments_hash()— an 18-byte digest (1.x: 9), each of the nine features a signed 16-bit fixed-point value (PH_DIGEST_KIND_VECTOR16,PH_VECTOR16_SCALE) instead of an unsigned byte that discarded the sign of skewness. Useph_l2_distance()as before; it decodes the encoding. -
Block Mean Hash (BMH) — thresholds against the median of the block means, not their arithmetic mean, per the algorithm's own reference method. No code change needed beyond recomputing stored values. BMH values differ from OpenCV's
BlockMeanHash, which uses the mean. -
Radial hash — reimplemented after its source: a DCT over the 180-angle variance vector, pHash's gamma and blur defaults. No 1.x radial value carries over. Compare radial digests with
ph_radial_similarity()only:
ph_digest_t a, b;
ph_compute_radial_hash(ctx_a, &a);
ph_compute_radial_hash(ctx_b, &b);
double score;
ph_error_t err = ph_radial_similarity(&a, &b, &score);
/* score >= PH_RADIAL_PCC_THRESHOLD (0.9) is the algorithm's own similarity cutoff */
Radial tolerates a few degrees of rotation plus an exact half turn.
JPEG hashes move by a few bits: the accurate IDCT¶
The native JPEG decoder (libjpeg-turbo, the default CMake build) uses its accurate integer IDCT; 1.x used the fast one, whose approximation error showed up in the hashes. Any stored hash of a JPEG can differ from the 2.0 value by a few bits — usually within any matching threshold, but not bit-identical, so an exact-match lookup on stored JPEG hashes misses. There is no switch back; recompute stored JPEG hashes. Builds without libjpeg-turbo (stb_image decodes JPEG) and PNG/WebP images are unaffected.
Building and linking¶
PHASH_USE_TURBOJPEG is PHASH_USE_LIBJPEG_TURBO¶
The CMake option that selects the native JPEG decoder is named after the codec,
libjpeg-turbo. CMake reports the 1.x name as an unused variable and otherwise ignores
it, so -DPHASH_USE_TURBOJPEG=OFF builds with libjpeg-turbo; pass the new name:
In an existing build directory, the old cache entry can be dropped with
-UPHASH_USE_TURBOJPEG.
Shared library consumers must relink¶
Shared builds carry a versioned soname, SOVERSION = 2 (libphash.so.2 /
libphash.2.dylib). A binary linked against an unversioned 1.x shared library will not
pick up 2.0 at runtime — relink against the installed 2.x library.
The shared library exports exactly the functions of libphash.h. A 1.x shared build
also exported libphash's internal helpers, the stbi_* functions of its bundled
stb_image, and the libjpeg-turbo, libpng and libwebp symbols of the decoders linked into
it. Code that called any of those through libphash fails to link against 2.0; link the
library that provides them instead.
find_package(phash) and pkg-config¶
If your 1.x integration linked libphash by hand (raw -lphash plus manually-tracked
include paths and backend libraries), you can use either of these instead:
Both forms pull in whatever backend libraries (-lphash_jpeg, -lpng16, -lwebpdecoder,
-lz, ...) the installed build was configured with, so a static link does not need
that list tracked by hand.
Smaller build changes¶
- The test-only mock decoder is not compiled into the library. 1.x registered a
backend that claimed any buffer starting with
DE ADand "decoded" it into a 1×1 image; in 2.0 such a buffer isPH_ERR_UNSUPPORTED_FORMAT. It exists only for testing:-DPHASH_ENABLE_MOCK_BACKEND=ON, never in a shipped build. - The spng PNG backend is removed. A 1.x build configured with
-DPHASH_USE_SPNG=ON -DPHASH_USE_LIBPNG=OFFdecoded PNG with spng; in 2.0 CMake reportsPHASH_USE_SPNGas an unused variable. Drop the flag: the default build decodes PNG with libpng (-DPHASH_USE_LIBPNG=OFFleaves it tostb_image).