Photos quietly carry your location, your camera, the time you took them, and sometimes more.
imgstrip removes all of it, then checks its own work before it saves anything.
Quick start · How it works · Which switch? · Everyday use · Python · Web app · Limits
|
🧱 Rebuilt from pixels It decodes the image and writes a brand-new file. By default only pixel values cross over, so there is nothing left to leak. |
🔬 Checked twice A strict parser must account for every part of the new file, then the file is decoded again and compared with the pixels. If either check fails, nothing is written. |
|
👀 Look before you clean See what a photo gives away first: location, camera, serial numbers, time taken, hidden previews. Nothing changes until you say so. |
🕵️ Beyond metadata Data tacked on after the image, colour hidden under transparent pixels, and (opt-in) pixel and JPEG steganography and the camera's sensor fingerprint. |
|
🔒 Private by design The web app runs only on your computer, works offline, never writes your originals to disk and uploads nothing. |
🖼 Your formats JPEG, PNG, WebP, GIF (animated too), HEIC/HEIF, AVIF, TIFF and BMP, with 16-bit PNG and TIFF kept at full precision. |
Why imgstrip?
Most metadata removers delete the tags they recognize and copy the rest of the file untouched. Anything they don't recognize survives.
imgstrip works the other way around. It reads only the pixels and builds a brand-new file from them. By default nothing from the old file is copied over, so there is nothing left to leak. (The opt-in JPEG options --jpeg-lossless, --jpeg-canonical and --scrub-dct keep some of the picture's compressed data; see Good to know below.)
$ imgstrip --check holiday.jpg
FOUND holiday.jpg (JPEG 640x480): 4 item(s)
[metadata] EXIF: make=NIKON, model=COOLPIX P6000, date=2008:10:22 16:28:39
[metadata] GPS location: 43.467448, 11.885127
[hidden] Embedded EXIF thumbnail: 6702 bytes
[metadata] XMP: 4000 bytes
$ imgstrip holiday.jpg
cleaned holiday.jpg -> holiday.clean.jpg
removed: EXIF, GPS location, Embedded EXIF thumbnail, XMP
$ imgstrip --check holiday.clean.jpg
clean holiday.clean.jpg (JPEG 640x480)
Your original file is never touched. The clean copy is saved next to it.
Quick start
You need Python 3.10 or newer.
pip install imgstrip
Prefer to keep it out of your main Python setup? pipx install imgstrip and uv tool install imgstrip work too.
Then try it:
imgstrip photo.jpg # writes photo.clean.jpg
imgstrip-web # or use the point-and-click web app
Check which version you have:
$ imgstrip --version
imgstrip 1.2.3
Want to be sure the install works? Run imgstrip --self-test. It builds 21 booby-trapped test images, cleans them in every mode, and has independent image readers confirm the results.
Install from source instead, or add Content Credentials signing
git clone https://github.com/lloydsato/imgstrip.git
cd imgstrip
pip install .
Signing cleaned images with a Content Credential (C2PA) is optional and needs an extra, because it brings a native library:
pip install 'imgstrip[c2pa]'
How it works
flowchart LR
A["📷 Your photo"] --> B["Decode to<br/>raw pixels"]
B --> C["Write a<br/>brand-new file"]
C --> D{"Check 1<br/>is every part<br/>accounted for?"}
D -->|yes| E{"Check 2<br/>do the pixels<br/>still match?"}
D -->|no| X["⛔ Stop.<br/>Nothing is saved"]
E -->|yes| F["✅ Clean copy saved"]
E -->|no| X
If either check fails, nothing is written. Failing safe is the whole point.
(If you're reading this on PyPI and the diagram shows as plain text, the GitHub page renders it.)
Which switch do I need?
The defaults already remove metadata, hidden containers and appended data. The switches below are for specific worries.
| If you're worried about... | Use | Cost |
|---|---|---|
| What a photo gives away (location, camera, dates) | imgstrip photo.jpg |
JPEGs are re-encoded at quality 95 |
| A JPEG losing any quality at all | --jpeg-lossless |
Keeps the compressed picture data as it was, so use it for pictures you trust |
| Data hidden in a JPEG's table layouts, padding or markers | --jpeg-canonical |
Same pixels, rebuilt in one canonical form. DCT steganography survives |
| Messages hidden in the low bits of pixels (LSB) | --scrub-lsb(look first with --deep) |
Changes each value by at most 1/255. Invisible |
| Messages hidden in a JPEG's DCT coefficients (JSteg, F5, steghide, J-UNIWARD...) | --scrub-dct |
Costs picture quality. The report says how much |
| A photo being linked to your specific camera (sensor fingerprint) | --scrub-prnu |
Moves parts of the picture by a few pixels. Strong reduction, never proof |
| Colours that must stay exact | --icc sanitize |
Embeds a rebuilt, anonymous colour profile |
| Showing readers the image was sanitised, not altered | --c2pa |
Needs pip install 'imgstrip[c2pa]' and a signing certificate |
File names that leak dates (IMG_20240517_133700.jpg) |
--random-name |
None |
Everyday use
| I want to... | Run |
|---|---|
| See what a photo gives away, without changing it | imgstrip --check photo.jpg |
| Clean one photo | imgstrip photo.jpg |
| Clean a whole folder, including subfolders | imgstrip -r my-photos/ -o clean-photos/ |
| Replace the originals with clean versions | imgstrip -i photo.jpg |
| Give outputs random names | imgstrip --random-name photo.jpg |
| Clean a JPEG with no quality loss at all | imgstrip --jpeg-lossless photo.jpg |
| Convert while cleaning | imgstrip --format png photo.jpg |
| Preview what would happen, write nothing | imgstrip -n photo.jpg |
| Also search the pixels for hidden data | imgstrip --check --deep photo.png |
| Clean from a pipe | cat a.heic | imgstrip - --format jpeg > a.jpg |
Run imgstrip --help to see every option.
In scripts and CI
--check writes nothing and sets the exit status, so it works as a gate. Add --json for machine-readable output.
imgstrip --check --json photo.jpg
imgstrip --check uploads/*.jpg || echo "something here would leak"
| Exit status | Meaning |
|---|---|
0 |
Success, or (with --check) everything is clean |
1 |
At least one file failed |
2 |
Usage error |
3 |
--check found metadata or hidden data |
Use it from Python
from imgstrip import Options, StripError, strip_image
with open("photo.jpg", "rb") as f:
data = f.read()
try:
result = strip_image(data, Options(scrub_lsb=True))
except StripError as exc: # includes VerificationError: nothing was produced
raise SystemExit(f"could not clean it safely: {exc}")
with open("photo.clean.jpg", "wb") as f:
f.write(result.data)
print([str(item) for item in result.removed])
The engine is the same one the command line and the web app use, so it makes the same checks and fails the same way.
Web app
imgstrip-web
Drop photos onto the window and each one shows what it gives away. Click Clean, then download the results one by one, as a ZIP, or save them straight to a folder.
Open Options for the same switches as the command line: Deep scan, Keep JPEG data bit-exact, Scrub least-significant bits, Disrupt camera fingerprint, Sign with Content Credentials, plus the output format, quality and colour profile.
Options that do not apply to the images you added are greyed out, and ones that cannot be done for them are greyed out with a yellow warning triangle and the reason.
- Runs only on your computer (
127.0.0.1) and works offline. - Your originals are never written to disk, and nothing is uploaded anywhere.
- On a shared computer, start it with
imgstrip-web --no-browserand open the printed link yourself.
What it handles
| Formats | |
|---|---|
| Everyday photos | JPEG, PNG, WebP, GIF (animated too), HEIC/HEIF (iPhone), AVIF |
| Also supported | TIFF, BMP, and ICO, PSD, TGA, QOI, JPEG 2000 and other less common formats (these come out as PNG) |
| Not supported | Camera RAW files (DNG, CR2, NEF, ARW...). Export to JPEG, PNG or TIFF first. Vector formats (SVG, PDF, EPS) are also unsupported. |
Removed: EXIF, GPS, XMP, IPTC, comments, camera serial numbers, embedded thumbnails, depth maps, color profiles that name your device, data tacked on after the image, and colors hidden under transparent pixels.
Good to know
No tool can do everything, and it's better to know the limits up front.
- What's in the picture stays. Faces, street signs, reflections and landmarks can reveal more than any tag.
- Your camera's sensor fingerprint stays unless you use
--scrub-prnu. Even then, nobody can prove it's gone, and the scrub is not reliable against anyone who holds another photo of the same scene (a thumbnail, a second shot, the original document). Its strength was measured in simulation, not on real cameras, and it costs about 20 dB PSNR against the original; text can shift by several pixels. - File names can leak dates (
IMG_20240517_133700.jpg). Use--random-name(the web app has a Random file names switch). --checkis a structural check, not proof. It lists what a file's container holds. It does not look inside compressed picture data, so it cannot see data hidden in JPEG image data or in the pixels (use--deepfor the pixel tests).- A clean scan isn't proof. "Not detected" means the tests found nothing, not that nothing is hidden.
- Invisible watermarks built to survive re-saving (such as SynthID) live in the picture itself and may survive.
- JPEGs are re-encoded (quality 95) by default. Use
--jpeg-losslessto avoid any quality loss. That mode keeps the compressed picture data exactly as it was, so it doesn't clear steganography hidden in the image data itself, and it is meant for pictures you trust, not as a fix for a file the default mode refused. It does drop tables nothing uses and refuses a file that carries bytes no decoder reads. Re-encoding doesn't reliably clear it either: hiding tools that change zero coefficients mostly survive.--scrub-dctis built for that and destroys payloads read from the DCT coefficients, at a cost of roughly 33 to 39 dB of picture quality against the original. It never claims that nothing is hidden. See how it works. -iis not a secure erase. Old copies can linger in backups, cloud sync history and SSD wear-leveling.- Very grainy JPEGs may be refused by the default mode because the re-encoded copy would drift too far from the original, and imgstrip would rather refuse than save something it couldn't verify. Use
--jpeg-canonicalfor those (same pixels, rebuilt in one canonical form).
Learn more
The full technical reference covers every option, the exact list of what gets removed, how the sensor-fingerprint scrub was measured, optional Content Credentials (C2PA) signing for journalists, and how the project is tested.
| Read | What's in it |
|---|---|
| 📘 Technical reference | Every option, what gets removed and what changes, supported formats, how it is tested |
| 🧬 JPEG steganography workflow | When and how to use --jpeg-canonical and --scrub-dct |
| 🛰 Sensor fingerprint (PRNU) | What --scrub-prnu does and how well it works, measured |
| 🪪 Content Credentials (C2PA) | Signing sanitised images, trust, and what a credential can and cannot say |
Development
git clone https://github.com/lloydsato/imgstrip.git
cd imgstrip
pip install -e '.[test]'
pytest
The version lives in one place: __version__ in src/imgstrip/__init__.py. The packaging reads it from there, and so does this page: README.md is generated from docs/README.template.md by scripts/render_readme.py. After a version bump, CI refreshes the README on main; to do it by hand, run python scripts/render_readme.py (add --check to only verify). Edit the template, never the generated file.
License
imgstrip is free software, released under the BSD 3-Clause License.
Versions up to 1.2.0 were released under the GNU Affero General Public License v3.0 or later; 1.2.1 and later are BSD-3-Clause.
Questions, bug reports and odd files that break it are welcome in the issue tracker.
Metadata
Release files for imgstrip 1.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| imgstrip-1.2.3.tar.gz | 428.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| imgstrip-1.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 725.3 kB
Release files / imgstrip-1.2.3.tar.gz
| Download URL | imgstrip-1.2.3.tar.gz |
|---|---|
| Size | 428.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b335bbc67f8efb17dc09844183dd727e5b84c9d5a451e8b3ddb8033bbf0a43df
|
|
BLAKE2b-256 checksum How to use checksums |
531eaec7639196f87a7213b996b29dd6402159c7a9c8de3a43bb8d4c5e3c1930
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / imgstrip-1.2.3-py3-none-any.whl
| Download URL | imgstrip-1.2.3-py3-none-any.whl |
|---|---|
| Size | 296.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a70ee6f8c84e36f51e2e1b42fca2475790bd7b4b5aa9f29b8c779b69d7c533d8
|
|
BLAKE2b-256 checksum How to use checksums |
fbf8d45dbdffd2b0886944203adfc01e78d9fb46dd7b95a7971aca1f875b49cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for imgstrip 1.2.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for imgstrip-1.2.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.