certifipic
v1.0.0
Published
CertifiPic by Faraz Rahimi — image authenticity engine, SDK, and HTTP API.
Maintainers
Readme
CertifiPic
CertifiPic by Faraz Rahimi
Image authenticity lab and full API: localize edits on a photo, score how AI-generated it looks, and return JSON + PNG overlays you can drop into any app.
Copyright (c) 2026 Faraz Rahimi. Use it, study it, duplicate it, and call it from your own apps. The app UI does not need to mention CertifiPic or Faraz Rahimi. If the app is publicly documented (GitHub, docs, a paper), copy LICENSE exactly (including every co-author named in it) and cite this repo. Forks of the source must keep that exact license and credit.
Quick start
Library name: certifipic. Install from GitHub (this works today):
npm install github:FarazRahimi/CertifiPicnpm install certifipic will work only after the package is published to npmjs.com. It is not on the registry yet.
import { analyze } from "certifipic";
const result = await analyze(file, { colors: { ela: "#ff8800" } });Run the lab and HTTP API from this repo:
git clone https://github.com/FarazRahimi/CertifiPic.git
cd CertifiPic
npm install
npm run dev # lab UI → http://localhost:5173
npm run api # HTTP API → http://127.0.0.1:8787Analyze from any language:
curl -F [email protected] http://127.0.0.1:8787/v1/analyzeCustomize mark colors:
curl -F [email protected] -F options='{"colors":{"ela":"#ff8800"}}' http://127.0.0.1:8787/v1/analyzeFull request/response reference: docs/API.md.
What it is
CertifiPic is not a single “fake / real” neural net. It is a pipeline that:
- Reads file provenance (C2PA / IPTC / XMP / PNG text / JPEG comments).
- Classifies the raster as photograph, document (ink on paper), or graphic.
- Runs classic forensic maps: ELA, noise residual, edges, copy-move.
- On photos, runs supporting Fourier and sensor-grain screens.
- Fuses those into a risk score, regions, colored marks, and an AI-generation score (0–100) in the sidebar / API — not as an AI heatmap on the pixels.
The test webpage is a client. The engine lives in src/engine/. The public SDK is src/sdk.ts. The HTTP server is api/server.ts (same engine).
How the logic works
Algorithm map — every image walks this graph:
flowchart TD
A[Image bytes] --> B[Decode and scale<br/>maxDimension 1600]
B --> C[Provenance<br/>C2PA / IPTC / XMP / EXIF]
C --> D{File AI label?}
D -->|trainedAlgorithmicMedia| E[AI score ~98<br/>kind: full]
D -->|composite / generative fill| F[AI score high<br/>kind: partial]
D -->|no label| G[Classify type]
G --> H{photograph / document / graphic}
H -->|document| I[Paper-baseline ELA<br/>ignore letter strokes]
I --> J[Compact edit blob only]
J --> K[AI screens off]
K --> Z[JSON + PNG marks<br/>AI score in sidebar]
H -->|photograph or graphic| L[Source profile<br/>camera / resave / screenshot]
L --> M[ELA]
L --> N[Noise residual]
L --> O[Edges]
L --> P[Clone / copy-move]
L --> Q[Fourier FFT]
L --> R[Sensor grain]
M --> S[Fusion]
N --> S
O --> S
P --> S
Q --> T[AI score 0–100<br/>no AI heatmap]
R --> T
S --> U[Risk / verdict / regions]
T --> U
U --> V[Color washes<br/>ela / noise / clone]
V --> Z
E --> Z
F --> VDocument vs photo (why invoices are not treated like selfies):
flowchart LR
subgraph Photo
P1[ELA + noise + clone] --> P2[FFT + sensor]
P2 --> P3[AI score + edit marks]
end
subgraph Document
D1[Ink-on-paper check] --> D2[ELA vs paper only]
D2 --> D3[Mark the pasted patch<br/>not the whole page]
endThen the stages in order:
1. Decode and size
The file is decoded to an ImageBitmap and scaled so the longest side is maxDimension (default 1600). All maps share that raster. EXIF orientation is respected when the decoder provides it.
2. Provenance (first, when it exists)
src/engine/provenance.ts and src/engine/metadata.ts:
- EXIF / XMP via
exifr(camera make/model, software, timestamps, GPS, embedded thumbnail). - JPEG COM comments and PNG
tEXt. - IPTC Digital Source Type strings:
trainedAlgorithmicMedia→ fully AI (file label)compositeWithTrainedAlgorithmicMedia→ partly AI (file label)
- C2PA four-byte marker
c2pain the file header/trailer (presence only; signatures are not cryptographically verified in this build).
Generator names (midjourney, dall-e, …) are matched in metadata and filename only, not in JPEG entropy bytes (that caused false hits).
If the file still carries a real AI label, that wins. Pixel heuristics do not override a trainedAlgorithmicMedia stamp.
Many chat exports and screenshots strip these tags. Then the engine falls back to pixels.
3. Image type
src/engine/fusion.ts → classifyKind:
- Document: pale paper + short dark ink runs (letters), not just a bright room. Filename hints (
invoice,receipt, …) help. - Graphic: very low contrast / UI-like.
- Photograph: everything else.
Documents turn off clothing/FFT AI screens (type grids dominate a Fourier transform) and use a paper-baseline ELA: ignore letter strokes, keep compact compression blobs (a pasted dollar amount), not the whole page.
4. Source profile
src/engine/source.ts decides how much to trust each detector:
| Source | ELA | Notes | |---|---|---| | Camera JPEG with EXIF | Trusted | Best case | | Re-encoded / social JPEG | Weak | Double compression everywhere | | PNG / screenshot / clipboard | Not trusted | ELA lights up UI text; clone-search is more useful |
5. Error Level Analysis (ELA)
src/engine/ela.ts
Re-save the raster as JPEG at elaQuality (default 0.75) and take a per-pixel difference from the original. Patches that were already dirty, pasted, or saved at a different quality stand out.
Marks (src/engine/marks.ts) do not dump the raw ELA map on the photo. They isolate compact blobs relative to a local/paper baseline and paint a wash in colors.ela (default amber #e3b34d). You can change that hex in the API.
6. Noise residual
src/engine/noise.ts
High-pass residual vs a blur. Tile coefficient of variation flags splices where grain does not match. Hair/fabric with high contrast and grain is skipped so it does not look like a fake. Wash color: colors.noise.
7. Edges
src/engine/edges.ts
Sobel-style gradient magnitude. Documents are expected to be edge-heavy (type). Photos with wild spatial CV get a mild score. This is a supporting signal, not a sole verdict.
8. Clone / copy-move
src/engine/clone.ts
Block matching on a downscaled grid. A cluster of pairs with the same shift vector is a clone stamp. Repeating bricks/checkerboards are discarded when the pair list explodes. Two washes: source (clone-src, mint) and destination (clone-dst, red). Off on documents (repeated letters are not copy-move).
9. Fourier + sensor (photos only)
src/engine/spectrum.ts— 2D FFT. Grid / peakiness vs a natural 1/f slope. Buildings and type look “grid-like”; that is why this is off on documents and is not painted as an AI heatmap.src/engine/sensor.ts— residual smoothness (denoise / synthetic fields). Not a true PRNU camera fingerprint.
These numbers feed the AI score. They do not stripe the picture.
10. AI-generation score
src/engine/ai.ts
| ai.kind | ai.score | When |
|---|---|---|
| full | ~90–98 | File label, or (rare) whole-frame stack without a camera |
| partial | ~52–88 | File composite label, or a concentrated repeating/smooth patch (not the whole frame) |
| none | 0 | Default. Clean photos stay at 0 |
The product shows this as how AI generated (sidebar meter / ai.score). There is no AI heatmap overlay. Fully labeled AI still gets a FULLY AI GENERATED stamp on the lab canvas.
Invisible vendor watermarks are not read here and are not mentioned in the user-facing report.
11. Fusion and report
src/engine/fusion.ts weights ELA / noise / edges / clone / metadata by source type.
src/engine/report.ts writes a short plain-language note from those scores.
Repository layout
src/engine/ forensic pipeline (ELA, noise, clone, AI, fusion)
src/sdk.ts public JS/TS API (analyze + options)
src/App.tsx test lab UI
api/server.ts HTTP API (same engine, Node + canvas)
api/polyfill.ts OffscreenCanvas for Node
backend/ optional FastAPI sidecar (not the full engine)
docs/API.md HTTP + SDK reference
LICENSE CertifiPic License
NOTICE Exact LICENSE copy; fork credit
CONTRIBUTING.md Inbound contributions use the same licenseScripts
| Command | What |
|---|---|
| npm run dev | Vite lab on port 5173 |
| npm run api | HTTP API on port 8787 |
| npm run build | Typecheck + production lab build |
| npm run preview | Serve the production lab |
Optional Python sidecar:
cd backend
pip install -r requirements.txt
python main.py # http://127.0.0.1:8000License
CertifiPic License — see LICENSE and NOTICE.
- App UI: you do not have to show CertifiPic or Faraz Rahimi inside the product.
- Public docs / GitHub of a project that uses it: copy LICENSE exactly (verbatim, including every co-author named in it) and link to https://github.com/FarazRahimi/CertifiPic. Do not abridge or rewrite the license.
- Fork or publish changed source: keep an exact copy of this license (all co-author names), keep NOTICE, keep CertifiPic by Faraz Rahimi, and keep the repo link.
- Source copies: leave copyright headers, co-author names, and LICENSE in place.
- Name: do not call your product CertifiPic or claim Faraz Rahimi endorses it.
- Contributions: come in under the same license. No warranty. This license does not claim any patent.
