How to Tell If an Image Is AI Generated, and When the Check Stops Working, article cover in AI Basics on learnai24.com

How to Tell If an Image Is AI Generated, and When the Check Stops Working

Ask how to tell whether an image was made by AI and the answer you get almost everywhere is the same: check its Content Credentials. Right-click, open the metadata, look for the icon. The advice is correct as far as it goes.

What I could not find anyone checking is whether the credentials are still in the file by the time the image reaches you. So I checked. I signed an image with the C2PA Python library, put it through 18 things that routinely happen to a picture, and read the result each time.

No image operation preserved a valid credential. Not one. The only files that still verified were the ones where nothing had been done to the bytes at all.

What a Content Credential actually is

It is not a label or a flag. It is a signed block of data inside the image file, holding a record of what made the picture, a certificate, a signature, and a hash of the image data itself. The standard is C2PA; Content Credentials is the name used for it in products and by the project itself.

The hash is the part that decides everything below. The signature does not say “this image is genuine”. It says “whoever holds this certificate signed an asset whose data hashed to exactly this value”. Change the bytes that get hashed and the check fails, whether or not the picture looks any different.

The test

One signed JPEG, one control and 18 operations, each applied to the signed file directly rather than to the output of the previous step. After each one I read the file back with the same library and recorded what came out.

What was doneResultFile size
nothing, the signed file itself (control)valid347,314
copied byte for bytevalid347,314
renamed to .png, bytes untouchedvalid347,314
exiftool writes one EXIF fieldpresent but invalid347,422
jpegtran lossless optimize, metadata keptpresent but invalid335,692
jpegtran lossless rotate, metadata keptpresent but invalid350,550
jpegtran lossless optimize, metadata droppedgone256,510
JPEG re-encode at quality 95gone264,737
JPEG re-encode at quality 80gone114,847
JPEG re-encode at quality 60gone75,362
resized to 1080 px widegone90,082
cropped by 80 px on each edgegone146,192
rotated 90 degreesgone180,677
converted to PNG, which is what a screenshot also producesgone937,597
converted to WebPgone97,942
exiftool -all=, the usual way to strip metadatagone268,114
ImageMagick convertblock unreadable250,092
ImageMagick convert -stripgone170,910
ffmpeg re-encodegone157,208

Quality 95 is worth pausing on. That is the kind of re-encode an editor performs when you open a file and press save, and it is not a quality setting anyone would call aggressive. The credential does not survive it.

The middle rows are the interesting ones

Two outcomes are unsurprising once you see them. Untouched bytes keep the credential. Re-encoding removes it, because the credential is metadata and metadata is what image pipelines drop.

The third outcome is the one I had not seen written up. In three cases the credential was still in the file, the signature still checked out, and the verdict was still invalid. The library gives the reason:

assertion.dataHash.mismatch
asset hash error, name: jumbf manifest,
error: hash verification( Hashes do not match )

Nothing about the credential changed and, in two of those three cases, nothing about the picture changed either. What changed were the bytes that get hashed. I checked the pixels directly: after the lossless jpegtran optimize and after exiftool wrote a single unrelated EXIF field, the decoded image is identical to the signed original, mean absolute difference 0.0000 across every pixel. The file on disk is not identical, and that is all the hash cares about.

The third of those rows, the lossless rotate, is a weaker example and I want to be straight about why. jpegtran turns the image without re-encoding it, but 1067 pixels is not a multiple of the 16 pixel block JPEG works in, so the output is not a clean rotation of the input. That row belongs in the invalid column, but it is not a case of an unchanged picture.

The point stands on the other two: “this file has Content Credentials” and “this file’s Content Credentials check out” are different statements, and only the second one means anything. A tool that reports the first as a green tick is telling you very little.

There is a fourth outcome, and I did not expect it. ImageMagick 6.9.12 without the -strip flag carried the credential data into the new file but rewrote the segment structure around it, and the reader could not parse the result at all: Verify: invalid embedded file box. Not “no credentials”, not “invalid credentials”, but an error. Whether a verifier should report that as missing, as broken, or as an error is a question the standard leaves open, and my test cannot answer it either.

Why the credential is large, and why that is a choice

Signing the 268,132 byte test photo produced a 347,314 byte file. The credential costs 79,182 bytes, close to 30 percent on top of the picture.

Almost all of that is one thing. The library embeds a thumbnail of the image in the manifest by default, and in my run that thumbnail is a 1024 by 683 JPEG of 66,206 bytes, which is 83.6 percent of the whole credential. The signature and the certificate are a rounding error next to it. So the 30 percent is a default of this signing setup rather than a property of the standard, and a signer who turns the thumbnail off produces something far smaller.

It is worth saying that size is not why the credential disappeared in my test. exiftool -all= and convert -strip remove it because it is metadata, and they would do that at any size.

How to read a credential check

Look for validity, not presence. A verifier should answer three things separately: is a credential there, does it match the asset, and who signed it. A tool that only answers the first cannot tell an untouched original from a file someone re-saved.

The absence of a credential tells you nothing. In my test, 12 of 18 operations removed it outright from a file that had one, and three more left it there but broken. A file with no credential is the same file whether the picture came from a camera, from a model, or from a screenshot of either.

Ask what the signature covers. It covers the bytes that were hashed and the identity in the certificate. It does not cover whether the picture is an accurate depiction of anything.

What the people behind the standard say about this

This is a known problem, not one somebody missed. The Content Authenticity Initiative’s documentation on Durable Content Credentials states that platforms hosting media assets “might remove C2PA manifest data”, for example when their software does not support the standard yet.

Their answer is to stop relying on the embedded manifest alone. Durable Content Credentials add two more mechanisms: an invisible watermark actively inserted into the content, and a fingerprint passively computed from it. As their documentation puts it, if a copy of the manifest data is stored in an online database, the watermark or fingerprint can be used to find it again after the embedded copy is gone.

My test only touches the embedded manifest. The other two mechanisms exist precisely because of what happens in the table above, and they are exactly what I could not measure.

What this does not show

Every tool I used is C2PA-unaware. Pillow, ImageMagick 6, ffmpeg, jpegtran and exiftool have no support for the standard, so in one sense the result is what you would predict. That is the point: those are the tools that sit inside ordinary image pipelines. A C2PA-aware editor behaves differently, and re-signs rather than drops.

No real platform. Everything here is a local stand-in. Resize, re-encode and strip are the operations upload pipelines are known to perform, but I did not push a file through WhatsApp, Instagram or X and read the result. That is a separate measurement and it is worth doing.

A self-signed certificate. I generated my own, so “valid” here means the manifest matches the asset and the signature verifies. The reader records signingCredential.untrusted alongside that, because my certificate is on nobody’s trust list. Nothing in the survival question depends on that, since the hash check runs regardless, but you will see the code if you run the script.

One synthetic image, one format. A single generated JPEG. Two rows depend on its specific dimensions or on a library default, as noted above. Other containers embed the manifest differently, and video or PDF would behave differently again.

No chains. Each operation ran on the signed original. Chaining them would change nothing, because after the first operation there is no valid credential left to break.

FAQ

So can I tell whether an image was made by AI?

Sometimes, and never from absence. A credential that validates and names an AI generator is strong evidence about how the image was made. A file with no credential is the normal state of almost everything, and it supports no conclusion in either direction.

Does a screenshot remove Content Credentials?

A screenshot keeps the pixels and writes a new file, which is the same operation as the PNG conversion in my table, and that came out with no credential at all. I did not test an actual screen capture tool on a real desktop, so treat this as the mechanism rather than a measurement of your operating system.

If the credential is invalid, does that mean the image was faked?

No. It means the file is not byte for byte what was signed. In my test, invalid came from a lossless optimize and from writing one EXIF field, neither of which changed a single pixel. Invalid means “do not rely on this credential”, not “the picture is a lie”.

Why is the credential so large?

In my run, 79,182 bytes, and 66,206 of those are an embedded thumbnail that the library adds by default. Turn the thumbnail off and the credential is a small fraction of that.

Can I rerun this?

Yes. The script is here. It generates a certificate, builds the test image, signs it, runs all 18 operations and prints the table, along with the version of every tool it used. You need openssl, the c2pa-python and pillow packages, and the exiftool, ImageMagick, jpegtran and ffmpeg command line tools. It takes well under a minute.

The short version

Content Credentials work exactly as specified, and in my test not a single image operation left one intact. A save at quality 95 removed it. A PNG conversion removed it. A lossless optimize left it in the file and broke it, which is the case worth knowing about, because a file can carry credentials that do not verify. If you are going to check an image, check whether the credential validates rather than whether one exists, and treat a missing credential as the ordinary state of a file rather than as a verdict about the picture.

Sources and method, 14 September 2026

Every number on this page came from one run of a script I wrote for it, and that script is published here. Versions, as printed by the script: c2pa-python 0.37.10, Pillow 12.2.0, exiftool 12.76, ImageMagick 6.9.12-98, jpegtran from libjpeg-turbo 2.1.5, ffmpeg 6.1.1.

Test image: a 1600 by 1067 synthetic picture generated by the script, saved as JPEG at quality 95, 268,132 bytes. Signing certificate: a self-signed ES256 certificate generated with OpenSSL at run time, so validity means the manifest matches the asset and the signature verifies; the reader also records signingCredential.untrusted, since that certificate is on no trust list. The manifest declares one c2pa.created action with the IPTC digital source type for trained algorithmic media. Signed file: 347,314 bytes, of which the credential is 79,182 and the library’s default embedded thumbnail is 66,206.

Each of the 18 operations was applied to the signed file directly, not in a chain, and each output was read back with the same library. “Valid” means the reader reports validation state Valid; “present but invalid” means a manifest was found and the state is not Valid, with failure code assertion.dataHash.mismatch in all three cases; “gone” means the reader reports no manifest; “block unreadable” means the reader raised Verify: invalid embedded file box. Pixel comparisons are mean absolute difference on the decoded luminance channel.

Byte counts shift by a byte or two between runs, because a fresh certificate and a fresh ECDSA signature are generated each time, and by more on a different Pillow or libjpeg build. The result column is what reproduces.

Standard: the C2PA specification. The quoted wording about platforms removing manifest data, and the description of watermarks and fingerprints, come from the Content Authenticity Initiative’s documentation on Durable Content Credentials.

What this page does not claim: that any specific platform strips credentials, since I tested local tools rather than uploads; that the results carry over to video, PDF or other containers; that C2PA-aware software behaves this way, since none of the tools I used supports the standard; or that a missing credential says anything about whether an image is genuine.

Similar Posts