The document understanding engine for AI applications.
Parse PDF · DOCX · XLSX · PPTX · HTML · email · scanned pages & images into structured, AI-native JSON — with built-in OCR, an actual image per page, and retrieval-ready RAG chunks.
Measured on ParseBench (parsebench.ai) — the document-parsing benchmark for AI agents: ~2,078 human-verified pages from real enterprise documents (insurance, finance, government), scored across five capability dimensions by fully deterministic, rule-based evaluation — no LLM judge, no subjectivity. Benchmark run: July 5, 2026.
No models. No GPUs. No cloud calls. Byte-identical output on every run.
| Parser | Overall | Tables | Charts | Content Faith. | Sem. Formatting | Visual Ground. |
|---|---|---|---|---|---|---|
| Cognita | 🥇 38.8 | 35.1 | 🥇 8.6 | 🥇 69.7 | 🥇 50.0 | 🥇 30.5 |
| PyMuPDF4LLM | 30.9 | 🥇 36.7 | 1.6 | 60.9 | 44.6 | 10.7 |
| MarkItDown | 18.6 | 15.8 | 2.0 | 64.5 | 0.9 | 9.9 |
| PyMuPDF (text) | 16.0 | 0.0 | 0.0 | 68.3 | 1.0 | 10.9 |
| pypdf | 14.9 | 0.0 | 0.0 | 62.5 | 0.9 | 10.9 |
How does that compare to ML-model and cloud-API systems?
Heavier classes for perspective — these ship neural layout models or run in the cloud:
| Parser | Class | Overall | Tables | Charts | Content | Formatting | Grounding |
|---|---|---|---|---|---|---|---|
| Azure Document Intelligence | cloud API | 59.6 | 86.0 | 1.6 | 84.9 | 51.9 | 73.8 |
| Docling-models | local + ML models | 50.7 | 66.4 | 52.8 | 66.9 | 1.0 | 66.1 |
| AWS Textract | cloud API | 47.9 | 84.6 | 6.0 | 74.8 | 3.7 | 70.4 |
Where Cognita punches above its weight class:
- Semantic formatting 50.0 — beats Docling's ML pipeline (1.0) and AWS Textract (3.7); within two points of Azure (51.9). Bold, italics, super/subscripts, underline, strikethrough and title hierarchy survive parsing because the engine tracks per-span styling and page decoration graphics.
- Charts 8.6 — ahead of both classic cloud IDP APIs (Azure 1.6, Textract 6.0).
- Content faithfulness 69.7 — above Docling (66.9), with zero model in the loop.
Methodology: official ParseBench harness, full dataset (all five dimensions, 2,553 test documents, 100% parse success), Cognita's optional local Tesseract OCR enabled for scanned pages. Comparison rows from the official ParseBench leaderboard as of July 5, 2026. Cognita's scores are self-reported from that harness — reproducing coming soon with parse-bench run cognita.
Cognita is not a text extractor and not an LLM wrapper. It works like a compiler: every format is lowered into one common Intermediate Representation (IR) — headings, paragraphs, tables, lists, images, reading order, bounding boxes, confidence scores — and every output (Markdown, HTML, plain text, chunks, page images) is rendered from that IR. Send a file, get structure.
- Any document, one API — born-digital PDF, DOCX, XLSX, PPTX, HTML and email (.eml), plus scanned PDFs and photos (PNG/JPEG/GIF). Format detection is content-based; misnamed files just work.
- OCR built in — a zero-dependency pure-Go engine out of the box; drops in Tesseract automatically when installed for production-grade scans.
- Pixel-faithful images — every page as an actual raster, and every embedded image extracted the way viewers show it: JPEGs byte-for-byte, alpha-masked cutouts composited with real transparency.
- RAG chunks that respect structure — never split mid-block, carry heading breadcrumbs, and point back to exact pages and block IDs.
- Dependency-free engine — the whole parsing engine (every format, OCR, layout, rendering) is pure standard library; the PDF parser is written from first principles. The only third-party dependency is the MongoDB driver, and only when the optional API-key auth layer is enabled.
- Deterministic — identical input produces byte-identical output. No model in the loop unless you add one.
Format-specific parsers (the PDF one is built from first principles — no PDF library) lower documents into positioned primitives; OCR fills in text for scanned pages; a shared layout engine recovers headings, lists, tables, columns, and reading order; everything lands in the IR; exporters, the chunker, the page renderer and the store all consume only the IR. Every arrow is an interface — new formats, outputs and pipeline stages plug in without touching the rest.
# Local development without a key store:
COGNITA_AUTH_ENABLED=false go run ./cmd/server # serves on :8080With authentication on (the default), point COGNITA_MONGO_URI at MongoDB and mint a key with go run ./cmd/genkey first — see Authentication.
Open http://localhost:8080/ for the built-in console (drag & drop any document; paste your key into the API-key field when auth is on), or call the API:
curl -s -F "file=@q3-report.docx" http://localhost:8080/v1/parseFull response (real output, shortened only inside the base64 string):
{
"document": {
"id": "324b1b83a80680d9",
"source_format": "docx",
"metadata": { "title": "Q3 Report", "author": "Finance Team", "page_count": 1 },
"pages": [
{
"number": 1,
"blocks": [
{
"id": "p1-b0",
"type": "heading",
"page": 1,
"reading_order": 0,
"confidence": 1,
"level": 1,
"text": "Q3 Report",
"spans": [ { "text": "Q3 Report" } ]
},
{
"id": "p1-b1",
"type": "paragraph",
"page": 1,
"reading_order": 1,
"confidence": 1,
"text": "Revenue grew 18% this quarter.",
"spans": [
{ "text": "Revenue grew " },
{ "text": "18%", "style": { "bold": true } },
{ "text": " this quarter." }
]
},
{
"id": "p1-b2",
"type": "table",
"page": 1,
"reading_order": 2,
"confidence": 1,
"table": {
"rows": [
{ "cells": [
{ "blocks": [ { "id": "p1-b3", "type": "paragraph", "page": 1, "reading_order": 3, "confidence": 1, "text": "Metric", "spans": [ { "text": "Metric" } ] } ] },
{ "blocks": [ { "id": "p1-b4", "type": "paragraph", "page": 1, "reading_order": 4, "confidence": 1, "text": "Value", "spans": [ { "text": "Value" } ] } ] }
] },
{ "cells": [
{ "blocks": [ { "id": "p1-b5", "type": "paragraph", "page": 1, "reading_order": 5, "confidence": 1, "text": "Revenue", "spans": [ { "text": "Revenue" } ] } ] },
{ "blocks": [ { "id": "p1-b6", "type": "paragraph", "page": 1, "reading_order": 6, "confidence": 1, "text": "$4.2M", "spans": [ { "text": "$4.2M" } ] } ] }
] }
]
}
}
]
}
]
},
"markdown": "# Q3 Report\n\nRevenue grew **18%** this quarter.\n\n| Metric | Value |\n| --- | --- |\n| Revenue | $4.2M |\n",
"text": "Q3 Report\n\nRevenue grew 18% this quarter.\n\nMetric\tValue\nRevenue\t$4.2M\n",
"metadata": { "title": "Q3 Report", "author": "Finance Team", "page_count": 1 },
"page_images": [
{ "page": 1, "mime": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAABMgAAAYw…", "source": "rendered" }
]
}What each top-level field gives you:
| Field | What it is |
|---|---|
document |
The full IR: pages → blocks (heading/paragraph/table/list/image/…) with IDs, reading order, confidence, styles, and — for PDFs and scans — bounding boxes. Embedded images live in each page's images array (id, name, MIME, dimensions, bbox, base64 data); the image block in the reading order points at its entry via image_ref |
markdown / text |
Ready-to-use renderings (GFM tables, lists, inline styles) |
metadata |
Title, author, dates, page count, plus custom keys (e.g. ocr_engine when OCR ran) |
page_images |
One raster per page: "source": "original" = the untouched scan, "source": "rendered" = a preview drawn from the IR |
Embedded images come out exactly as a PDF viewer would show them:
- Unmasked JPEGs pass through byte-for-byte —
image/jpegentries are the literal bytes the author embedded, verifiable by checksum. - Masked images are composed, not leaked — cutouts stored as base image +
/SMask(soft alpha) or stencil/Maskare composited into a PNG with real transparency. This matters: encoders fill masked-out regions with junk pixels that only the mask hides, so extracting the base alone would expose garbage. - Raw rasters decode across colorspaces — DeviceGray/RGB/CMYK, Indexed palettes, ICCBased, CalGray/CalRGB, at 1–8 bits per component — and re-encode losslessly as PNG.
Current limits: JPEG2000 streams are extracted as-is (image/jp2, no mask composition) and CCITT-fax images return placement metadata without pixels.
Extracted text is normalized for every format before it reaches the IR, so the same clean output flows into JSON, Markdown, plain text and RAG chunks: typographic ligatures (fi→fi, ffl→ffl) expand, non-breaking and other exotic Unicode spaces fold to a regular space, and zero-width and soft-hyphen artifacts are dropped. PDF fake-bold double-strikes are de-duplicated so text isn't doubled, and the Markdown exporter escapes formatting characters in content so literal *, _, ` and [ ] are never misread as styling.
Upload as multipart/form-data with a file field, or send the file bytes as the raw request body — both work on every parsing endpoint.
| Endpoint | Description |
|---|---|
POST /v1/parse |
file → IR + Markdown + text + metadata + page images |
POST /v1/chunk |
file or IR JSON → structure-aware RAG chunks |
POST /v1/export?format=markdown|html|text|json |
IR JSON → re-rendered output |
POST /v1/documents |
parse and store; returns the document summary |
GET /v1/documents |
list stored documents |
GET /v1/documents/{id} |
full IR of a stored document |
GET /v1/documents/{id}/export?format= |
render a stored document |
GET /v1/documents/{id}/chunks?max_chars=&overlap= |
chunk a stored document |
GET /v1/documents/{id}/pages/{n}/image?scale= |
one page as an image |
DELETE /v1/documents/{id} |
remove a stored document |
GET /health · GET /version |
liveness · version + supported formats |
Errors are uniform: { "error": { "code": 415, "message": "…" } } — 415 unsupported format, 422 unparseable document, 413 too large, 404 unknown document/page, 401 missing/invalid API key.
When auth is enabled (the default), every route except GET /health and GET / (the console) requires an API key. Pass it in the X-API-Key header (a Bearer Authorization header also works):
curl -s -H "X-API-Key: cog_…" -F "file=@report.pdf" http://localhost:8080/v1/parseKeys live in a MongoDB collection (api_keys by default); a key is valid if a document with a matching key field exists. There is deliberately no endpoint to issue keys — provisioning is an operator action run from a trusted host with the genkey command:
go run ./cmd/genkey -label "analytics team"
# → cog_9f8a1c… (also inserted into MongoDB)If the key store is unreachable at startup, the server refuses to boot rather than run unauthenticated. Set COGNITA_AUTH_ENABLED=false to disable auth entirely (e.g. for local development).
| Parameter | Values | Default | Effect |
|---|---|---|---|
include |
comma list of document,markdown,text,metadata,page_images |
all | Select response fields — e.g. ?include=markdown,metadata for a light response |
image_data |
omit |
include | Strip embedded image bytes from the IR (dimensions/MIME stay); page images are unaffected |
# Markdown only — smallest useful response
curl -s -F "file=@contract.pdf" "http://localhost:8080/v1/parse?include=markdown"
# Full IR but without megabytes of embedded image base64
curl -s -F "file=@brochure.pdf" "http://localhost:8080/v1/parse?include=document&image_data=omit"Accepts a file upload, or a previously returned IR document as Content-Type: application/json. Options: max_chars (default 2000) and overlap (default 0).
curl -s -F "file=@q3-report.docx" "http://localhost:8080/v1/chunk?max_chars=800"{
"document_id": "324b1b83a80680d9",
"count": 1,
"chunks": [
{
"id": "c0",
"text": "Q3 Report\n\nRevenue grew 18% this quarter.\n\nMetric\tValue\nRevenue\t$4.2M",
"heading_path": ["Q3 Report"],
"pages": [1],
"block_ids": ["p1-b1", "p1-b2"],
"chars": 69
}
]
}Chunks never split inside a block (oversized paragraphs break at sentence boundaries), carry the heading breadcrumb in force, and cite the exact pages and block_ids they came from — so retrieval answers can be grounded back to the source, visually if you pair them with page_images.
ID=$(curl -s -F "file=@handbook.pdf" http://localhost:8080/v1/documents | jq -r .id)
curl -s "http://localhost:8080/v1/documents/$ID/export?format=html" # render later
curl -s "http://localhost:8080/v1/documents/$ID/chunks?max_chars=1200" # chunk later
curl -s "http://localhost:8080/v1/documents/$ID/pages/1/image" -o p1.png # page as imageAny page with no native text but a page-covering raster — a scanned PDF page or an uploaded photo — is OCR'd automatically, and the recognized words flow through the exact same layout analysis as digital text. You do nothing special; the response looks identical, plus metadata.custom.ocr_engine tells you OCR ran, and page_images returns the original scan untouched.
| Engine | When | Quality |
|---|---|---|
builtin |
always available, pure Go, zero setup | clean machine-printed text |
tesseract |
picked up automatically when the binary is installed | production-grade scans, 100+ languages |
The default auto mode means: install Tesseract (brew install tesseract / apt install tesseract-ocr) and restart — recognition quality upgrades with no config or code change.
Document { id, source_format, metadata, pages }
Page { number, width, height, unit, blocks, images?, preview? }
Block { id, type, page, bbox?, reading_order, confidence, level?, text,
spans, style?, children?, table?, image?, image_ref?, metadata? }
Span { text, style?, link? }
PageImage { id, block_id, name, mime, width, height, bbox?, alt_text?, data }
Block types: heading paragraph table image list list_item quote code header footer caption page_number
- IDs are deterministic (
p2-b7= page 2, reading order 7) — same input, byte-identical output, stable references. - Confidence is honest — explicit structure (DOCX) reports
1.0; layout and OCR heuristics report what they know. - Geometry survives — PDF and scan blocks keep bounding boxes (top-left origin, points), so answers can be highlighted on the page image.
- Images are page assets — every embedded image lives once, in its page's
imagesarray with placement and payload; the reading-order block references it byimage_ref. No hunting through the block tree, no duplicated base64. - Open extension — every block carries a
metadatamap; enrichments attach without breaking consumers.
Copy configs/config.example.json → configs/config.json, or use environment variables (env wins):
| Setting | Env | Default | |
|---|---|---|---|
addr |
COGNITA_ADDR |
:8080 |
listen address |
max_upload_mb |
COGNITA_MAX_UPLOAD_MB |
64 |
upload size cap |
ocr.engine |
COGNITA_OCR_ENGINE |
auto |
auto · builtin · tesseract · off |
ocr.tesseract_path |
COGNITA_TESSERACT_PATH |
$PATH |
explicit binary location |
ocr.languages |
COGNITA_OCR_LANGUAGES |
eng |
Tesseract language spec, e.g. eng+deu |
storage.max_documents |
— | 100 |
in-memory store capacity (oldest evicted) |
auth.enabled |
COGNITA_AUTH_ENABLED |
true |
require an API key on all routes but /health and / |
auth.mongo_uri |
COGNITA_MONGO_URI |
mongodb://localhost:27017 |
MongoDB connection string |
auth.mongo_db |
COGNITA_MONGO_DB |
cognita |
database holding the keys |
auth.collection |
COGNITA_MONGO_COLLECTION |
api_keys |
key collection |
- Scale — streaming page-at-a-time parsing for very large documents; CJK/RTL script support
- Structure accuracy — vector-graphics capture for lattice table detection and XY-cut reading order
- Outputs — knowledge-graph and search-index exporters
- Storage — disk and database backends
- AI enrichment — optional stage over the IR: semantic labels, image captions, entities, summaries