DEV Community

Super Lewis
Super Lewis

Posted on

How to Read C2PA and IPTC AI Labels From Image Bytes in Node.js

Most AI images carry a note from the tool that made them, and you can read it with about 60 lines of TypeScript. The hard part is not parsing it; it is knowing what its absence means, which is nothing at all.

sparkpix.ai's AI Image Detector is a web tool that estimates whether an image was AI-generated by reading provenance labels in the file first and then scoring the pixels. This post walks through how the first layer works in Node.js, how the two layers combine into a verdict, and the one upload bug that silently deletes the evidence.

Disclosure: I build sparkpix.ai and apimodels.app, and both appear below. This post was drafted with AI assistance for structure and wording; the code is from the sparkpix codebase (Next.js App Router, Node 24, September 2026).

Two kinds of label live inside AI images

AI tools write provenance into the file in two standard ways, and a detector should look for both.

  • C2PA Content Credentials. A signed manifest, defined by the Coalition for Content Provenance and Authenticity, that records which tool produced the file and what was done to it. OpenAI, Adobe Firefly and Microsoft embed one. It sits in a JUMBF box: JPEG APP11 segments, a caBX chunk in PNG, a C2PA chunk in WebP.
  • IPTC Digital Source Type. An XMP field whose value trainedAlgorithmicMedia means "created by a generative model" and compositeWithTrainedAlgorithmicMedia means "edited with one". Google writes it into Gemini and Imagen images, and C2PA action assertions reuse the same vocabulary.

When either is present, the question is settled: the maker declared it.

A byte scan is enough to detect presence

You do not need a full C2PA validator to answer "does this file claim to be AI?". We deliberately scan bytes and report presence plus the claimed generator, not signature validity:

export function readMetadataSignals(buf: Buffer): MetadataSignals {
  const latin = buf.toString('latin1')
  const c2pa =
    (latin.includes('jumb') && latin.includes('c2pa')) ||
    latin.includes('caBX') ||
    /C2PA[\s\S]{0,8}jumb/.test(latin)
  const composite = latin.includes('compositeWithTrainedAlgorithmicMedia')
  const generated = !composite && latin.includes('trainedAlgorithmicMedia')
  let generator = c2pa ? claimGenerator(buf) : null
  if (!generator && /Made with Google AI/i.test(latin)) generator = 'Google AI'
  return { c2pa, generator, aiSource: composite ? 'composite' : generated ? 'generated' : null }
}
Enter fullscreen mode Exit fullscreen mode

Note the order of the two IPTC checks: compositeWithTrainedAlgorithmicMedia contains trainedAlgorithmicMedia as a substring, so the composite test has to run first or every AI-edited photo reads as fully generated.

The generator name comes from the C2PA claim, which is CBOR. Newer (v2) claims store it as claim_generator_info: [{ name: "ChatGPT" }]; older (v1) claims use a single claim_generator string such as Adobe_Photoshop/25.0 adobe_c2pa/0.7.6. Reading a CBOR text string by hand is a few lines, because the first byte tells you the length:

function readCborText(buf: Buffer, at: number): string | null {
  const b = buf[at]
  let len = -1
  let start = at + 1
  if (b >= 0x60 && b <= 0x77) len = b - 0x60      // length in the type byte
  else if (b === 0x78) { len = buf[at + 1]; start = at + 2 }            // 1-byte length
  else if (b === 0x79) { len = buf.readUInt16BE(at + 1); start = at + 3 } // 2-byte length
  if (len <= 0 || len > 200 || start + len > buf.length) return null
  return buf.toString('utf8', start, start + len)
}
Enter fullscreen mode Exit fullscreen mode

What this does not do: it does not verify the signature, so a forged manifest would pass. For "is this probably AI?" that is an acceptable trade; for legal provenance, use the official c2pa-rs or c2pa-node libraries.

Your upload pipeline may be deleting the evidence

The most common reason a detector finds no label is that your own code stripped it. Our site compresses every upload in the browser and re-encodes it to WebP before sending it to storage, which is right for an image editor and fatal for a detector: re-encoding drops the metadata segments where both labels live. The detector's upload path skips that optimizer for any file up to 10 MB and stores it byte-for-byte; larger files still get compressed, and the user is told the metadata check may miss.

Social platforms and chat apps do the same thing to everyone, and a screenshot creates a brand-new file. That is why a missing label proves nothing, and why a second layer is needed.

The second layer scores the pixels

When there is no label, a classifier looks at texture, noise and structure. We call an external model for this, apimodels.app's ai-image-detector (POST /v1/images/detections, synchronous, about $0.015 per image), which also reads C2PA itself and returns a 0-1 score:

  • Upload is stored unmodified, then two checks run.
  • Byte scan for C2PA and IPTC labels. If IPTC says generated, the verdict is AI Generated (99%+); if it says composite, Digitally Edited.
  • Otherwise the pixel classifier's score picks the band: 85-100 AI Generated, 60-84 Likely AI, 40-59 Uncertain, 16-39 Likely Real, 0-15 Real Photo.

A metadata declaration always wins over the pixel score, because it is the maker's own statement. Without either signal, the endpoint refuses to guess: if the classifier is down and the file has no label, the user gets an error and their credits back, not a made-up "Uncertain".

How accurate is it?

On our own set of 28 images, 14 AI images from GPT Image 2 and Gemini plus the 14 real photos they were modelled on, the combined detector got 27 right: no real photo was flagged and one AI image passed as real. Calls took 1.6 to 3.1 seconds. The classifier returned near-binary scores (0.99 or 0.001), and it named the generator on none of the 28, so attribution is best-effort.

That test is small and covers two generators. Don't read it as a benchmark.

When not to build this

If you need provenance that stands up in court or in a newsroom's publishing workflow, a byte scan is the wrong tool: use a real C2PA validator that checks signatures and the trust list. And if your images come from social media, skip the metadata layer's expectations entirely; almost everything will arrive stripped.

FAQ

Can I detect AI images without calling any API? Partly. The metadata layer is free and local, and it is decisive when a label exists. It answers nothing for stripped files, which is most images shared online.

Does SynthID show up in this scan? No. Google's SynthID is an invisible watermark in the pixels, not a metadata field, and there is no public API to read it.

Why is 40-59% labelled "Uncertain" instead of a lean? Because a classifier score near the middle carries almost no information, and presenting it as "slightly AI" invites people to act on noise.


Try the detector at sparkpix.ai/ai-image-detector. For the non-code version, including reverse image search and visual checks, see How to Tell If an Image Is AI-Generated.

Top comments (0)