Metadata-Version: 2.5
Name: imagefp
Version: 0.6.0
Summary: Decide whether two images (raster or EMF metafile) depict the same picture, tolerant of re-encoding, alpha-flattening, cropping, and metadata churn.
Project-URL: Homepage, https://github.com/tejanshsachdeva/imagefp
Project-URL: Repository, https://github.com/tejanshsachdeva/imagefp.git
Project-URL: Issues, https://github.com/tejanshsachdeva/imagefp/issues
Project-URL: Changelog, https://github.com/tejanshsachdeva/imagefp/blob/main/CHANGELOG.md
Author: Tejansh Sachdeva
License-Expression: MIT
License-File: LICENSE
Keywords: duplicate-detection,duplicate-image-finder,emf,image,image-comparison,image-deduplication,image-diff,image-hash,office-automation,perceptual-hash,phash,powerpoint,pptx,wmf
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: pillow>=9.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: test
Requires-Dist: pytest-cov>=4.0; extra == 'test'
Requires-Dist: pytest>=7.0; extra == 'test'
Description-Content-Type: text/markdown

# imagefp

Decide whether two images are **the same picture** — even when they were
re-encoded, alpha-flattened onto a background, cropped/padded differently,
or resized — without ever falsely claiming a match it isn't sure of.

Built for pipelines that re-save or re-export documents (PowerPoint, Word,
Excel, PDF) where the same source image commonly reappears in a different
container format, with different compression, or with its transparency
flattened away. Generic perceptual-hash libraries don't handle the
alpha-flattening or EMF-metadata-churn cases; this one does.

## Install

```bash
pip install imagefp
```

Raster comparison (`.png`, `.jpg`, `.gif`, `.bmp`, `.tiff`, `.webp`, ...)
requires [Pillow](https://pypi.org/project/Pillow/), which is installed
automatically as a dependency. EMF metafile comparison has no extra
dependency (WMF is not supported yet).

## Quick start

```python
from imagefp import images_match

with open("logo.png", "rb") as f:
    data_a = f.read()
with open("logo_export.jpg", "rb") as f:
    data_b = f.read()

match, reason = images_match(data_a, "logo.png", data_b, "logo_export.jpg")
print(match, reason)
# True "same picture -- shape 0/30, colour 3/18, aspect 0.000/0.11"
```

`images_match` is the one function most callers need. It takes the raw bytes
and the original file/part name (so it knows whether it's looking at a
raster or a vector metafile) for each image, and returns:

```python
(match: bool, reason: str)
```

`reason` is always populated, win or lose, so you can log *why* two images
were or weren't considered the same.

## How it decides "same picture"

1. **Classify** each file as raster (`.png`, `.jpg`, `.webp`, ...) or
   metafile (`.emf`, `.wmf`) by extension.
2. **Rasters** are compared with a perceptual fingerprint:
   - Apply EXIF orientation, then crop to actual content (ignore blank
     padding/margins; ink is detected from alpha or from a flat border colour).
   - Composite any transparency onto white, the same way PowerPoint does
     when it re-saves an image — so a transparent logo and its flattened
     export land in the same colour space.
   - Downsample to a 16×16 grid (colour thumbnail + a separate "ink mask"
     of where there's real content vs. background).
   - Compare in three gates, cheapest first: aspect ratio, then ink-mask
     shape, then colour — measured **only** over cells where both images
     actually have ink, so background/padding differences can't skew it.
3. **Metafiles** (`.emf` only) are compared by hashing drawing records,
   skipping the header (resize bounds), skipping non-EMF+ metadata comments,
   but **including EMF+ comment payloads** where Office stores GDI+ drawing
   data — so the same chart re-exported with new timestamps still matches,
   while an edit to the drawing (including EMF+ content) does not. The file
   must be a complete EMF (through `EMR_EOF`); truncated files are rejected.
4. **Anything uncertain returns "different."** Undecodable bytes, a
   raster-vs-metafile mismatch, missing bytes, unsupported WMF, or an
   incomplete/corrupt EMF — all reported as *not a match*, never as an
   uncertain match.

## What it does *not* do

- It cannot detect a match after a resize that rewrites an EMF's actual
  drawing coordinates (as opposed to just its header bounds) — that would
  require rendering the metafile to pixels, which is out of scope.
- `.wmf` files are legacy 16-bit metafiles and are **not supported yet**
  (`images_match` returns an explicit “WMF is not supported” reason). Only
  `.emf` metafiles are compared. Open an issue if you need WMF.
- This is a "is this the same picture" tool, not a general reverse-image
  search or similarity ranker — it returns a boolean, not a similarity
  score for ranking many candidates (though the `reason` string exposes the
  underlying distances if you want to build that yourself).

## API reference

| Function | Purpose |
|---|---|
| `images_match(data_a, name_a, data_b, name_b, *, shape=30.0, thumb=18.0, aspect=0.11)` | The main entry point. Returns `(match, reason)`. |
| `kind(name)` | `"raster"`, `"metafile"`, or `"other"`, by file extension. |
| `describe(data)` | Build a `Descriptor` (thumbnail + ink mask + aspect) for one raster image. |
| `same_image(a, b, ...)` | Run the three-gate comparison on two `Descriptor`s directly. |
| `emf_signature(data)` | SHA-256-derived signature of an EMF's drawing records, or `None`. |
| `thumb_distance`, `shape_distance`, `colour_distance` | The individual distance metrics, if you want to tune thresholds yourself. |

All public names are also importable directly from the `imagefp` package,
e.g. `from imagefp import Descriptor, kind, emf_signature`.

### Tuning thresholds

The three gates each take a threshold, exposed as keyword arguments on
`images_match`:

```python
images_match(data_a, name_a, data_b, name_b,
             shape=30.0,   # max ink-mask distance
             thumb=18.0,   # max masked colour distance
             aspect=0.11)  # max |log(aspect ratio difference)|
```

Lower values are stricter (fewer false positives, more false negatives);
higher values are looser. The defaults were tuned for documents re-exported
by PowerPoint/Office tooling — you may want to widen `thumb` slightly for
very lossy JPEG re-compression, or tighten `aspect` if your corpus has many
same-content images at deliberately different aspect ratios (e.g. a banner
vs. a thumbnail of unrelated art that happens to share a palette).

## Development

```bash
git clone https://github.com/tejanshsachdeva/imagefp.git
cd imagefp
pip install -e ".[dev]"
pytest
```

## License

MIT — see [LICENSE](LICENSE).