# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.3] - 2026-06-23

### Added

- **`fingerprint_image(&DynamicImage)` API**: New method on both `ImageFingerprinter` and `FingerprinterContext` that accepts an already-decoded `DynamicImage` directly, skipping the decode step entirely. Useful for video frames or in-memory compositions. `image::DynamicImage` is re-exported from the crate root.

- **`Display` impl for `ImageFingerprint` and `MultiHashFingerprint`**: Enables `format!("{}", fp)` for logging and string serialization. `ImageFingerprint` formats as `exact_hex:global_hex:block0,block1,...,block15`. `MultiHashFingerprint` formats as `exact_hex|ahash_global|phash_global|dhash_global`.

- **`fingerprint_stream()` streaming batch API**: New `ImageFingerprinter::fingerprint_stream(paths)` accepts any iterator of paths and returns a lazy iterator of `(path, Result)` pairs. Reads and fingerprints one image at a time with zero upfront memory, using a single `FingerprinterContext` internally for buffer reuse.

- **`no_std`-compatible comparison layer**: The `similarity` module (`Similarity`, `compute_similarity`, `hamming_distance`, `hash_similarity`) now uses only `core` primitives and the `subtle` crate. Consumers that only need to compare pre-computed fingerprints (embedded targets, WASM) can use the comparison types without `std`.

### Changed

- **Cached `CpuExtensions` on `FingerprinterContext`**: `FingerprinterContext` now reads the static `CpuExtensions` detection once at construction and stores it as a private `has_simd: bool` field for future SIMD dispatch. Previously each `Preprocessor::new()` call dereferenced the `OnceLock` cache. Non-breaking: the field is private. Closes #26.

- **SIMD-accelerated `rgb_to_grayscale` with AVX2/NEON/SSE4.1 intrinsics**: The BT.601 luma conversion on the preprocess hot path now dispatches to a `_mm_shuffle_epi8`-based SSE4.1 path on x86_64 (covers AVX2 as a superset) and a `vld3q_u8`-based NEON path on aarch64, falling back to the existing scalar implementation otherwise. Output is bit-identical to the scalar baseline, verified by a parity test (`test_rgb_to_grayscale_simd_matches_scalar`). Closes #23.

- **Pre-allocated DCT scratch buffers on `FingerprinterContext`**: The row/column/hash buffers previously allocated on the stack inside every `compute_phash` invocation (and `dct2_32`'s FFT buffers) are now hoisted into a per-context `DctScratch` owned by `FingerprinterContext`. Eliminates ~85 KiB of repeated stack-frame setup across a 17-call multi-hash pass. Public `compute_phash` / `compute_phash_from_64x64` signatures are unchanged. Closes #25.

- **Fixed `perceptual_distance` overflow with custom weights**: `MultiHashFingerprint::compare_with_config()` now normalizes the weighted distance by the sum of algorithm weights, keeping the value bounded to [0, 64] regardless of weight configuration. Previously, non-unit weight sums could produce distances exceeding 64.

- **Early dimension rejection before full decode**: `decode_image_with_config()` now reads image dimensions via `ImageReader::into_dimensions()` before performing the full decode. Oversized or undersized images are rejected without allocating the full pixel buffer.

- **Eliminated double-resample in block hashing**: `compute_ahash_from_64x64` and `compute_dhash_from_64x64` now resample directly from 64×64 to 8×8 / 9×8 in a single pass instead of the previous 64→32→8 two-pass approach. Eliminates 32 intermediate buffer allocations and bilinear passes per multi-hash fingerprint.

- **Zero-copy source image in preprocessing**: `normalize_as_slice()` now uses `ImageRef` (borrowed) for RGB8 images instead of cloning the entire pixel buffer. Non-RGB8 images still convert as before.

- **Removed redundant constant-time comparisons in multi-hash scoring**: Extracted `compute_score_only()` internal helper. `compare_with_config()` no longer performs 3 redundant `ct_eq` exact-hash comparisons on sub-fingerprints (the outer check already handles it).

- **Fixed `ImageFingerprint::is_similar` to use block hashes**: Now uses the full weighted similarity formula (40% global + 60% block hashes) instead of only the global hash distance. Consistent with `MultiHashFingerprint::is_similar` behavior. May change threshold behavior for callers relying on the old global-only comparison.

### Notes

- No fingerprint format, binary layout, or hash semantic changes. `FORMAT_VERSION` remains `1`.

### Performance

- `fingerprint_single_phash`: **-4.4%** (3.11 ms → 2.97 ms) — driven by DCT scratch reuse (#25).
- `fingerprint_multi`: **-6.0%** (3.45 ms → 3.30 ms) — combined effect of #25 (DCT scratch reuse) and #23 (SIMD rgb_to_grayscale).
- `grayscale_only`: within measurement noise — the bench measures the full `normalize_as_slice` pipeline where Lanczos3 resize dominates; the SIMD gain on `rgb_to_grayscale` is amortized in that path.
- `compare_multi`: within measurement noise — #28 (branchless block similarity) was reverted after benchmarking showed no improvement on the measured path; the issue's threshold target was calibrated against a different baseline.

## [0.4.2] - 2026-05-10

### Added

- **Zero-copy persistence example**: Added `examples/zero_copy_persistence.rs`, showing `bytemuck::cast_slice` persistence for `MultiHashFingerprint` slices plus `FORMAT_VERSION` validation before casting bytes back.

- **Feature-combination CI coverage**: Added CI coverage for no-default builds and individual `serde`, `parallel`, and `tracing` feature combinations so optional-feature regressions are caught before release.

### Changed

- **Eliminated double image decode** in `decode_image_with_config()`: Previously called `into_dimensions()` (which decodes the header) followed by `load_from_memory()` (which decodes the full image again). Now decodes once and validates dimensions from the decoded `DynamicImage`. ~30-40% latency reduction on the decode path.

- **Removed `expect()` panics from PHash**: `compute_phash()` and `compute_phash_from_64x64()` now return `Result<u64, ImgFprintError>` instead of panicking on DCT failures. The library now truly upholds its "no panics on malformed input" guarantee.

- **Removed block-level rayon parallelism**: The 16 per-block hash computations within each algorithm are now sequential. The outer algorithm-level `rayon::join` (AHash/PHash/DHash) is preserved. Eliminates ~48 rayon task spawns per fingerprint that cost more in scheduling overhead than they saved in compute.

- **Optimized RGB conversion in preprocessing**: `normalize_as_slice()` now skips the `to_rgb8()` conversion allocation when the decoded image is already `ImageRgb8` (common for JPEG inputs).

- **Reject zero-norm embedding vectors at construction**: `Embedding::new()` now returns `InvalidEmbedding` for all-zero vectors instead of deferring the error to `semantic_similarity()`.

- **Added `Display` impl for `HashAlgorithm`**: Enables `format!("{}", algo)` without requiring `Debug` formatting.

- **Added `PartialOrd` impl for `Similarity`**: Enables sorting similarity results by score (e.g., `results.sort_by(|a, b| a.partial_cmp(b).unwrap())`).

- **`PreprocessConfig` validation**: `decode_image_with_config()` now rejects configs where `min_dimension > max_dimension` with a clear error instead of producing confusing downstream failures.

- **Documented `subtle` crate rationale**: Added inline documentation explaining why constant-time comparison is used for BLAKE3 hash equality (defense-in-depth for network-exposed fingerprint checks).

- **Removed dead `local-embedding-model` feature flag**: Was just an alias for `local-embedding` with no additional gating.

- **Improved fingerprinting hot-path buffer reuse**: `FingerprinterContext` now keeps the normalized 256x256 grayscale buffer owned by the preprocessor during fingerprinting instead of moving it into a temporary `GrayImage`. This avoids a repeated 64 KiB allocation/copy pattern in repeated and batch fingerprinting without changing hash semantics or public APIs.

- **Refined optional `tracing` instrumentation**: Removed duplicate wrapper-level spans from static convenience methods, added stage-level timing events for exact hash, decode, normalize, region extraction, block extraction, and hash computation, and added success/failure counts to batch tracing events.

### Fixed

- **No-default feature testing with `tracing`**: Marked the `serialize` example as requiring the `serde` feature so `cargo test --no-default-features --features tracing` no longer fails on an unrelated serialization example.

### Notes

- No fingerprint format, binary layout, or hash semantic changes. `FORMAT_VERSION` remains `1`.

## [0.4.1] - 2026-04-27

### Added

- **`pub const FORMAT_VERSION: u32 = 1`** at crate root, plus `ImageFingerprint::format_version()` and `MultiHashFingerprint::format_version()` accessors. Persist alongside fingerprint bytes (or in a sidecar manifest) and refuse comparison across mismatched versions to guard against algorithm-version drift. The constant only changes when the algorithm output changes; layout drift is caught at compile time independently.

- **`bytemuck::Pod + Zeroable` derives** on `ImageFingerprint` and `MultiHashFingerprint` with `#[repr(C)]`. Enables zero-copy persistence:

  ```rust
  use imgfprint::MultiHashFingerprint;
  let bytes: &[u8] = bytemuck::cast_slice(&fingerprints);
  // ...write to disk / mmap / send over the wire...
  let back: &[MultiHashFingerprint] = bytemuck::cast_slice(bytes);
  ```

  Unblocks UCFP's `index` crate to store millions of fingerprints without serde-roundtrip overhead.

- **Compile-time layout assertions** — `const _` size checks that `ImageFingerprint` is exactly 168 bytes and `MultiHashFingerprint` is exactly 536 bytes. Any accidental field reorder or padding addition fails the build, so consumers relying on `bytemuck::cast_slice` can't be silently broken.

### Changed

- **`ImageFingerprint` and `MultiHashFingerprint` now derive `Copy`** (required by `bytemuck::Pod`). Trade-off: move-by-value silently memcpys 168 / 536 bytes; prefer `&Fingerprint` borrows in hot loops where this matters. Purely additive change — nothing that compiled before fails to compile now.

- **`#[repr(C)]`** added to both fingerprint types for stable cross-version binary layout. The default `repr(Rust)` layout was already padding-free at 168 / 536 bytes, so this is a guarantee, not a layout change.

### Notes for UCFP integrators

Storing fingerprints: cast a `&[MultiHashFingerprint]` to `&[u8]` with `bytemuck::cast_slice` before writing to your index. Persist the `FORMAT_VERSION` constant alongside (in your manifest or as a sidecar header). On read, verify the version matches before casting back. The layout-stability assertions guarantee that within a `FORMAT_VERSION`, the byte representation is identical across builds and platforms with the same endianness.

## [0.4.0] - 2026-04-27

### Added

- **`MultiHashConfig` for fully tunable similarity scoring**: Lets integrators (UCFP, downstream pipelines) shift trade-offs without forking the crate. Knobs:
  - Inter-algorithm weights: `ahash_weight`, `phash_weight`, `dhash_weight` (defaults 0.10 / 0.60 / 0.30 reproduce the previous hardcoded blend)
  - Intra-algorithm weights: `global_weight`, `block_weight` (defaults 0.40 / 0.60)
  - `block_distance_threshold` (default 32 of 64)

  Setting any algorithm weight to `0.0` removes it from the score — covers the "skip this hash" use case without restructuring `MultiHashFingerprint`. Score is clamped to `[0.0, 1.0]`.

  New API: `MultiHashFingerprint::compare_with_config(&self, other, &MultiHashConfig)`. Existing `compare()` and `compare_with_threshold()` are preserved as defaulted wrappers — no behavior change for existing callers. `DEFAULT_*_WEIGHT` and `DEFAULT_BLOCK_DISTANCE_THRESHOLD` constants are re-exported.

- **`PreprocessConfig` for tunable decode-time guards**: Replaces the hardcoded `MAX_INPUT_BYTES` (50 MiB) / `MAX_DIMENSION` (8192) / `MIN_DIMENSION` (32) constants. Tighten on untrusted ingest paths, widen for trusted batch jobs.

  New APIs accept `&PreprocessConfig` on both the bytes and the path entry points:
  - `decode_image_with_config(bytes, &cfg)` — also re-exported at crate root
  - `ImageFingerprinter::fingerprint_with_preprocess(bytes, &cfg)`
  - `ImageFingerprinter::fingerprint_path_with_preprocess(path, &cfg)`
  - `FingerprinterContext::fingerprint_with_preprocess(&mut self, bytes, &cfg)`
  - `FingerprinterContext::fingerprint_path_with_preprocess(&mut self, path, &cfg)`
  - `FingerprinterContext::fingerprint_with_algorithm_and_preprocess(&mut self, bytes, algo, &cfg)`

  The same config gates **both** the pre-read file-size guard and the decode-time dimension/byte guards, so the tightened limits aren't silently bypassed via the path API. `DEFAULT_MAX_INPUT_BYTES`, `DEFAULT_MAX_DIMENSION`, `DEFAULT_MIN_DIMENSION` constants are re-exported.

- **`compute_similarity_with_weights`**: New public function in `core::similarity` exposing the global/block weight split for callers that work with raw `ImageFingerprint`s.

- **`Hash` derive on fingerprint types**: `ImageFingerprint` and `MultiHashFingerprint` now derive `Hash`, enabling their use as `HashMap` / `HashSet` keys for deduplication workflows. All fields are stack-allocated integer arrays, so the derive is a pure structural addition with no runtime cost.

- **`fingerprint_path` convenience methods**: New methods on both `ImageFingerprinter` (static) and `FingerprinterContext` accept an `AsRef<Path>` and handle the file read internally. Variants:
  - `ImageFingerprinter::fingerprint_path(path)` — multi-algorithm
  - `ImageFingerprinter::fingerprint_path_with(path, algorithm)` — single-algorithm
  - `FingerprinterContext::fingerprint_path(&mut self, path)` — multi-algorithm with buffer reuse
  - `FingerprinterContext::fingerprint_path_with(&mut self, path, algorithm)` — single-algorithm with buffer reuse

  File size is validated via `metadata().len()` against the configured `max_input_bytes` *before* any read happens, so oversized files are rejected without being pulled into memory.

- **`ImgFprintError::IoError` variant**: New error variant for I/O failures (missing files, unreadable paths, oversized inputs). Includes a `From<std::io::Error>` impl. Backwards-compatible because `ImgFprintError` is `#[non_exhaustive]`.

### Changed

- **`compare_with_threshold` is now a wrapper over `compare_with_config`** — same scores, same defaults, simpler internals.

### Removed

- Internal `MAX_INPUT_BYTES`, `MAX_DIMENSION`, `MIN_DIMENSION` constants in `imgproc::decode`. Their `DEFAULT_*` public successors carry the same values; behavior is unchanged for callers who never imported the privates.

### Notes for UCFP integrators

Per-algorithm DCT/grid/hash-bit reconfiguration (different `dct_size`, `block_grid`, `hash_bits`) is **not** in 0.4.0 — those would require turning the static `[f32; 32*32]` and `[[f32; 64*64]; 16]` buffers into dynamically-sized vectors through every hash function and the preprocessor. That's a 0.5.0 surgery.

## [0.3.3] - 2026-03-28

### Fixed

- **Double image decode in `decode_image()`**: Eliminated redundant full image decode when checking dimensions. Previously `into_dimensions()` followed by `load_from_memory()` decoded the image twice. Now dimension checking uses the same reader without discarding it.
- **Post-EXIF dimension validation**: Added dimension re-validation after applying EXIF orientation transforms. A 9000x100 image rotated 90° now correctly rejected instead of silently producing out-of-limit dimensions.
- **Per-item context allocation in parallel batch**: `fingerprint_batch` and `fingerprint_batch_with` with the `parallel` feature were creating a new `FingerprinterContext` per image instead of per rayon task. Now uses `map_init` to cache context per worker, reusing preprocessor and hasher across images.
- **NaN/infinity threshold handling in `is_similar`**: Both `ImageFingerprint::is_similar()` and `MultiHashFingerprint::is_similar()` now clamp threshold to [0.0, 1.0] before comparison. Previously, NaN thresholds silently returned incorrect results in release builds (where `debug_assert!` is stripped).
- **Bilinear resample identity case**: Added fast-path `copy_from_slice` when source and destination dimensions are identical, skipping unnecessary floating-point interpolation.

### Changed

- **Performance optimizations**:
  - Use hardware POPCNT via `count_ones()` for Hamming distance (replaces byte-level lookup table)
  - Cache ONNX `RunnableModel` in `LocalProvider` to eliminate per-inference model clone + optimization overhead
  - Removed redundant O(n) finiteness re-validation in `semantic_similarity()` (invariant already guaranteed by `Embedding::new()`)
  - Removed `#[repr(align(64))]` from `ImageFingerprint` and `MultiHashFingerprint` (32 and 192 bytes wasted padding respectively)

- **API improvements**:
  - Added `compute_block_similarity_with_threshold()` for configurable block distance threshold
  - Added `compute_similarity_with_threshold()` for custom block matching strictness
  - Added `MultiHashFingerprint::compare_with_threshold()` for threshold-aware comparison

- **Code quality**:
  - Removed dead code: no-op `apply_orientation()` wrapper in preprocess (EXIF handled in decode module)
  - Renamed `rgb_to_grayscale_simd` to `rgb_to_grayscale` (was not actual SIMD, misleading name)
  - Deduplicated `ImageFingerprinter::fingerprint_batch_chunked` (now delegates to `FingerprinterContext`)
  - Fixed clippy warnings (type_complexity)
  - Added SAFETY documentation for `unsafe set_cpu_extensions()` block explaining CPU feature detection precondition
  - Improved DCT `expect()` messages with specific failure context
  - Clarified `#[allow(dead_code)]` annotation on public `compute_block_similarity()` convenience function
  - Added `#[must_use]` to constructor and accessor methods (`FingerprinterContext::new()`, `HashAlgorithm::hash_bits()`, `HashAlgorithm::max_distance()`, `LocalProviderConfig` presets)
  - Added `Eq` derive to `ImageFingerprint` and `MultiHashFingerprint` (all fields are `Eq`-compatible)
  - Downgraded `#[inline(always)]` to `#[inline]` on `hash_similarity()` and `hamming_distance()` to respect compiler heuristics
  - Added numeric separators to CLIP normalization constants for readability

## [0.3.2] - 2025-03-18

### Changed

- **Performance optimizations**:
  - Cache-line aligned fingerprint structs (64-byte) for CPU cache efficiency
  - Pre-computed popcount lookup table for faster Hamming distance
  - Shared bilinear resampling across AHash, DHash, PHash (removed 3 duplicate implementations)
  - SIMD-friendly chunked grayscale conversion
  - Stack-allocated buffers in PHash DCT (removed RefCell overhead)
  - Use `total_cmp` for f32 comparison (faster than partial_cmp)

- **Bug fixes**:
  - Fixed buffer reclamation order bug in image preprocessing
  - Fixed `apply_orientation()` unnecessarily cloning entire image - now uses `Cow::Borrowed` to avoid expensive clone when no EXIF orientation transform is needed
  - Fixed SIMD CPU extension detection being performed on every `Preprocessor::new()` call - now cached globally using `OnceLock` for better performance
  - **Fixed DCT-II implementation** in PHash: Added proper twiddle factors for mathematically correct DCT-II computation
  - **Fixed duplicate thread-local contexts**: Both `fingerprint()` and `fingerprint_with()` now share a single module-level TLS slot instead of creating separate ones
  - **Fixed unsafe memory operations**: Replaced `unsafe { set_len() }` with safe `resize()` in preprocessing buffers
  - **Fixed EXIF orientation handling**: Now reads EXIF orientation from raw bytes before decoding and applies transformations (rotate/flip) to decoded image
  - **Fixed similarity algorithm consistency**: `MultiHashFingerprint::compare()` now includes block-level similarity (60% weight) for consistent crop resistance with single-algorithm mode
  - **Fixed error handling in DCT**: Changed `unwrap()` to proper `Result` propagation in `dct2_32()`

- **API improvements**:
  - Made `hash_similarity()` and `hamming_distance()` public utilities
  - Added fast-path for exact match in similarity comparison
  - Added compile-time assertions verifying `ImgFprintError` implements `Send + Sync` for safe use in async and multi-threaded contexts
  - **Added model ID support to embeddings**: `Embedding::new_with_model()` allows tagging embeddings with model identifiers to prevent comparing incompatible models
  - **Fixed serde feature gating**: `serde` crate is now properly optional (not compiled unless feature enabled)

- **Code quality**:
  - Fixed clippy warnings (needless_range_loop, manual_clamp)
  - Added `kamadak-exif` dependency for EXIF orientation parsing
  - Updated bincode dev-dependency comment documenting v2 API changes

## [0.3.1] - 2025-03-16

### Added

- **New examples**:
  - `single_algorithm.rs` - Demonstrates using specific hash algorithms
  - `error_handling.rs` - Shows error type pattern matching

- **CONTRIBUTING.md**: Added semver policy documentation
  - Versioning scheme (MAJOR.MINOR.PATCH)
  - Breaking change definitions
  - Deprecation policy (one minor version before removal)
  - MSRV policy

- **Observability**: Added optional `tracing` feature for production debugging
  - `#[instrument]` attributes on all fingerprint methods
  - Timing spans for performance monitoring
  - Batch processing metrics (processed/failed counts)
  - Enable with `cargo build --features tracing`

### Changed

- **Improved API consistency**:
  - Removed misleading `Default` derive from `HashAlgorithm`
  - Added `Debug` derive to `FingerprinterContext` and `Preprocessor`
  - Added module-level documentation to `core`, `hash`, and `imgproc` modules

- **Improved error handling**:
  - Standardized error constructors in `decode.rs`
  - Added `image_too_small()` helper constructor
  - Added detailed `## Errors` sections to each `ImgFprintError` variant with trigger conditions

- **Improved test coverage**:
  - Added 13 edge case tests covering non-square images, boundary sizes, color spaces, and all supported formats

- **Improved embedding safety**:
  - Added NaN/infinity re-validation in `semantic_similarity()`
  - Added performance note to `Embedding::vector()` for O(n) clone warning

## [0.3.0] - 2025-03-15

### Added

- **AHash (Average Hash) implementation**:
  - New `HashAlgorithm::AHash` variant
  - Average-based perceptual hash algorithm
  - Bilinear resampling from 32x32 to 8x8
  - 64-bit hash output with 16 block-level hashes
  - Fastest algorithm (~0.3ms)

- **Three-algorithm fingerprinting**: Compute AHash, PHash, and DHash simultaneously
  - Weighted similarity: 10% AHash + 60% PHash + 30% DHash
  - New `MultiHashFingerprint::ahash()` accessor
  - Parallel computation of all three algorithms

- **Chunked batch processing**: New `fingerprint_batch_chunked()` method
  - Processes images in configurable chunks to limit memory usage
  - Callback-based API for immediate result processing
  - Available on both `ImageFingerprinter` and `FingerprinterContext`
  - Prevents unbounded memory consumption when processing large batches

### Changed

- Replaced SHA2 with BLAKE3 for exact hashing
  - 6-8x faster hashing performance
  - Still provides 32-byte exact match hashes

- **BREAKING**: Renamed `global_phash()` to `global_hash()` in `ImageFingerprint`
  - More accurately reflects that the method returns whichever algorithm was used (PHash or DHash)
  - Updated all documentation and examples accordingly

## [0.2.0] - 2025-03-13

### Added

- **Multi-algorithm fingerprinting support**: Compute both PHash and DHash simultaneously
  - New `HashAlgorithm` enum with `PHash` and `DHash` variants
  - New `MultiHashFingerprint` struct containing both algorithm results
  - Parallel computation of PHash and DHash using rayon for optimal performance
  - Weighted similarity comparison (60% PHash + 40% DHash) for improved accuracy

- **DHash (Difference Hash) implementation**:
  - Gradient-based perceptual hash algorithm
  - Bilinear resampling from 32x32 to 9x8
  - 64-bit hash output with 16 block-level hashes
  - Faster than PHash (~0.5ms vs ~1.5ms)

- **Single algorithm mode**:
  - `ImageFingerprinter::fingerprint_with(algorithm)` for computing specific algorithm only
  - More efficient when only one hash type is needed

- **Batch processing improvements**:
  - `fingerprint_batch()` now returns `MultiHashFingerprint` for each image
  - New `fingerprint_batch_with(algorithm)` for batch processing with specific algorithm

- **Comprehensive test coverage**:
  - Tests for multi-algorithm determinism
  - Tests for single algorithm mode
  - Tests for weighted comparison
  - Tests for algorithm accessors

- **Updated fuzz targets**:
  - `algorithm_fuzz` - Tests individual PHash and DHash algorithms
  - `batch_fuzz` - Tests batch processing
  - Updated existing targets for new API

### Changed

- **BREAKING**: `ImageFingerprinter::fingerprint()` now returns `MultiHashFingerprint` instead of `ImageFingerprint`
  - Migration: Use `fingerprint_with(HashAlgorithm::PHash)` for old behavior
  - Or use `fingerprint().phash()` to get PHash results from multi-hash

- **BREAKING**: `fingerprint_batch()` now returns `Vec<(S, Result<MultiHashFingerprint, ImgFprintError>)>`
  - Migration: Use `fingerprint_batch_with(HashAlgorithm::PHash)` for old behavior

- Updated all examples to use new multi-algorithm API
- Updated documentation with multi-algorithm examples and guides
- Updated benchmarks to compare multi vs single algorithm performance

### Removed

- **BREAKING**: Removed `src/hash/blocks.rs` - generic block hashing function
  - Block hashing is now handled per-algorithm inline in the fingerprinter
  - This was an internal API not exposed publicly

## [0.1.3] - Previous Release

### Features

- SHA256 exact hash for byte-identical detection
- PHash (Perceptual Hash) using DCT-based algorithm
- Block-level hashing with 4x4 grid for crop resistance
- SIMD-accelerated image resizing (AVX2/NEON)
- Parallel batch processing with rayon
- Context API for high-throughput scenarios
- Serde support for JSON/binary serialization
- Semantic embeddings via external providers
- Local ONNX inference (optional feature)

[Unreleased]: https://github.com/themankindproject/imgfprint-rs/compare/v0.4.2...HEAD
[0.4.2]: https://github.com/themankindproject/imgfprint-rs/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/themankindproject/imgfprint-rs/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/themankindproject/imgfprint-rs/compare/v0.3.3...v0.4.0
[0.3.3]: https://github.com/themankindproject/imgfprint-rs/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/themankindproject/imgfprint-rs/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/themankindproject/imgfprint-rs/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/themankindproject/imgfprint-rs/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/themankindproject/imgfprint-rs/compare/v0.1.3...v0.2.0
[0.1.3]: https://github.com/themankindproject/imgfprint-rs/releases/tag/v0.1.3
