Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Image Perceptualhash Laravel Package

travail/image-perceptualhash

Generate perceptual hashes from images and compare them by Hamming distance to spot duplicates and visually similar pictures. Integrates with Laravel and Eloquent for storing hashes and finding near matches in your app.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a small, dependency-light implementation of a DCT-based perceptual hash (pHash): you hand it an image, it hands you a 64-bit fingerprint that stays stable across resizing, mild recompression, and small color shifts. That is the entire value proposition — it is not an image manipulation library, not a storage abstraction, and not a Laravel package. There is no service provider, no facade, no config file, and no auto-discovery.

Minimal steps

  1. Install it (it is on Packagist but has not been tagged since 2015 — pin the exact version):
    composer require travail/image-perceptualhash:*
    
    If the tag is unusable on your PHP version, fork it and add a repositories VCS entry rather than editing vendor/.
  2. Confirm the runtime requirement: it leans on ext-gd (imagecreatefromjpeg/png/gif, imagecreatetruecolor, imagecopyresampled). Verify with php -m | grep gd and check your php.ini memory_limit — hashing a 12MP JPEG is not free.
  3. Read exactly two files first: the ImageHash entry class (the public API surface) and the DCT helper (the math). Everything else is plumbing. Note the return type — older builds return the hash as an integer or a binary/hex string depending on version, and you need to know which before you design a column.
  4. Smoke-test outside Laravel first, with a tinker or plain script:
    $hash = \ImageHash::hash(storage_path('app/public/sample.jpg'));
    var_dump($hash); // note: int, binary string, or hex string?
    
  5. Your first real use case is almost always near-duplicate detection in a media library: compute the hash on upload, persist it next to the image record, and compare new uploads against existing hashes with a Hamming distance threshold.

Where to look first in a Laravel app

  • app/Services/ImageHashService.php — your wrapper, not theirs.
  • database/migrations/*_add_perceptual_hash_to_media_table.php — the storage decision (see Gotchas; this is the one irreversible choice).
  • An ImageObserver or a UploadImage action/job — the single write path for hashes.

Implementation Patterns

1. Wrap it, don't sprinkle it. The package is a static-style utility. Bind a thin adapter so you can swap the implementation later (you will — see Gotchas) and so tests can fake it:

// app/Services/PerceptualHash.php
namespace App\Services;

class PerceptualHash
{
    public function hash(string $absolutePath): string
    {
        // Normalize the return type to a 16-char hex string.
        return str_pad(dechex((int) \ImageHash::hash($absolutePath)), 16, '0', STR_PAD_LEFT);
    }

    public function distance(string $a, string $b): int
    {
        return substr_count(decbin(hexdec($a) ^ hexdec($b)), '1');
    }
}
// app/Providers/AppServiceProvider.php
$this->app->singleton(\App\Services\PerceptualHash::class);

2. Compute on write, compare on read. Hashing belongs in the upload pipeline, ideally queued so a bulk import does not block a request. If you use Storage disks, remember GD needs a local path — either resolve the disk to a path (Storage::disk('local')->path($key)) or stream to tempnam() and unlink in a finally:

$tmp = tempnam(sys_get_temp_dir(), 'phash');
file_put_contents($tmp, Storage::disk('s3')->get($key));
try { $hash = $this->hasher->hash($tmp); } finally { @unlink($tmp); }

3. The duplicate-detection workflow. On upload: hash → query candidates → Hamming compare → decide. Exact-match is a cheap where('phash', $hash). Near-match needs a bounded candidate set:

$candidates = Media::where('id', '!=', $media->id)
    ->where('phash_prefix_1', $hashPrefix1)   // 16-bit band
    ->orWhere('phash_prefix_2', $hashPrefix2)
    ->limit(500)
    ->get();

$duplicates = $candidates->filter(
    fn ($m) => $this->hasher->distance($hash, $m->phash) <= 6
);

Thresholds: 0 = byte-identical, 1–5 = same image re-encoded/resized, 6–10 = visually near-identical (crops no — pHash is sensitive to cropping), >12 = unrelated. Tune against your own corpus; do not copy a number from a blog post.

4. Banded indexing (the practical LSH trick). A 64-bit hash in a single column cannot be indexed for XOR. Split it into 4 × 16-bit bands and store them as separate indexed columns (or a media_phash_bands table). Two hashes within distance ≤3 must share at least one band, so querying on any matching band is a sound prefilter. This is the difference between a 40ms lookup and a 4-second table scan.

5. Backfill and maintenance. Ship an artisan command (media:hash-backfill --chunk=200) from day one. You will change the hashing logic or the column format at least once, and a chunked, resumable command is the only sane way to reprocess a library.

6. Testing. Build a small fixture set: one original JPEG, the same image at 50% quality, resized to 30%, converted to PNG, plus two genuinely different photos. Assert distance 0–5 for the first group and >12 for the second. That single test catches almost every regression — including the day someone "optimizes" by skipping EXIF rotation.

Gotchas and Tips

The abandonment problem is the headline. Last release was October 2015 — PHP 5.x era. On PHP 8.x expect deprecation notices, implicit nullable parameter warnings, and possibly each()/curly-brace offset syntax depending on the exact tag. Budget half a day for a compatibility pass, and seriously consider jenssegers/imagehash (maintained, namespaced, Composer-native, same algorithm family) as a drop-in behind your wrapper. Nine stars and zero dependents means you are the QA team.

Watch the return type. The hash comes back in whatever form that version chose — integer, binary string, or hex. Cast it once inside your wrapper and never again. If you store a binary string in a VARCHAR column you will lose bytes to encoding; store BINARY(8) or CHAR(16) hex, and be explicit about it in the migration.

Normalize before you hash. The library hashes what GD gives it. EXIF-rotated JPEGs (phone uploads) hash differently from their corrected versions; PNGs with alpha can composite against black and skew the DCT; progressively-encoded JPEGs occasionally behave differently. Run images through Intervention Image first — resize to a sane max dimension, correct orientation, flatten alpha onto white. This also fixes the memory blowups.

GD is a memory hog and fails loudly-to-quietly. A 6000×4000 image can need ~100MB+ during imagecreatefromjpeg. Always check the return value — on failure GD returns false and emits a warning, not an exception, and the hash of a corrupt file may come back as garbage rather than an error. Wrap calls in a try/catch and validate with getimagesize() before hashing.

It is not cryptographic and it is not stable across implementations. Two photos of the same beach can collide within your threshold; that is fine for duplicate detection and unacceptable for content moderation or deduplication of legal evidence. Never use the hash as a unique key. And do not assume hashes produced by this library match those from pHash, imagehash (Python), or jenssegers byte-for-byte — the DCT scaling and low-frequency coefficient selection vary. If you ever migrate, plan a full re-hash, not a comparison.

No Laravel integration points exist. No config to publish, no config/image-perceptualhash.php, no container bindings. If you want tuning parameters (hash size, high-frequency cutoff), you will be editing the library or forking it. Keep those edits in a fork with a VCS repository entry so composer update doesn't silently revert them.

Debugging technique. When distances look wrong, dump the raw 64-bit value as an 8×8 binary grid — perceptual hashes are visually interpretable, and you can spot immediately whether your preprocessing (grayscale conversion, resize) is the culprit versus the threshold. Also verify two hashes of the same file are identical; if not, something is nondeterministic (usually a temp-file race or a resource leak across a chunked loop).

Extension point. Your PerceptualHash interface is the only seam worth maintaining: hash(string $path): string and distance(string $a, string $b): int. That is a twenty-line contract that lets you swap in jenssegers/imagehash, a Python microservice, or a cloud vision API without touching a single caller.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource