Skip to content

Output Formats

Choose the format that matches your downstream processing. See the Configuration Reference for all output format options.

  • Content Format — output_format controls the content field: Plain, Markdown, Djot, HTML, JSON, DocTags, or a registered custom renderer such as DOT
  • Unified (default) — Plain text/Markdown, for LLM prompts and full-text search
  • Element-Based — Flat array of typed elements with metadata, for RAG chunking and semantic search
  • Document Structure — Hierarchical tree with explicit parent-child references, for knowledge graphs and structured apps
  • PDF Hierarchy — Font-size classification into heading levels (H1–H6) for PDFs
  • Image Output Formats — Normalize extracted images to PNG, JPEG, WebP, HEIF, or SVG

No configuration required. The result contains:

  • content — Full document text with minimal formatting
  • pages — Per-page breakdown for PDFs, DOCX, and PPTX
  • tables — Extracted tables in structured format
  • images — Image metadata and paths

Every Table in tables[] (and in pages[].tables) carries a table_id and columns, so consumers can reconcile a table’s markdown block in rendered content with its structured entry:

  • table_id (str | None) — a stable identifier the extraction pipeline assigns deterministically (for example "table-3", in document order), so the same input document always produces the same ids. Same-page fragments of one physical table are already merged into a single tables[] entry before ids are assigned, so in practice table_id is unique per entry rather than shared across several. A table split across a page boundary is not linked today — its per-page pieces get separate ids; sharing one id across page-boundary fragments is a documented future extension. None when the extractor did not assign one.
  • columns (list[str] | None) — the header cells (the first row of cells), populated even when the fragment’s own header row was merged away or lives in a sibling fragment. This makes a single fragment interpretable in isolation without needing table_id to look up siblings. None when no header row could be determined.

Set table_anchors=True on ExtractionConfig to have the renderer additionally emit a [TABLE:{table_id}] marker immediately before each table’s Markdown block in content, pages[].content, and chunks[].content — a lightweight anchor a consumer can regex for to splice structured table data into rendered output. Only takes effect when output_format is Markdown or Djot. Defaults to False, so existing output is byte-identical unless explicitly enabled.

table_anchors.py
from xberg import ExtractInput, ExtractionConfig, extract
config = ExtractionConfig(output_format="markdown", table_anchors=True)
output = await extract(ExtractInput(kind="uri", uri="report.pdf"), config=config)
result = output.results[0]
# result.content contains "[TABLE:table-1]" immediately before that table's markdown.
for table in result.tables or []:
print(table.table_id, table.columns)

output_format sets the format of the content field. It is independent of result_format (element-based / document structure). For OCR-backed scanned PDFs, non-Plain formats invoke the same geometry-derived structure heuristic (headings, lists) as native PDF extraction — but invoking it is not the same as it succeeding, and how much it recovers depends on the OCR backend. Measured on a 16-page scanned reference fixture with no layout detection, Tesseract promoted both headings and list items, Sceptre promoted few, and PaddleOCR promoted no headings at all. Enabling optional ML layout detection raises heading counts on every backend, but lowers list-item accuracy against ground truth. output_format != Plain is necessary but not always sufficient for structured OCR output.

Structure contract. output_format = Plain skips the structure heuristic entirely and returns raw text. Any other output_format always runs the heuristic; when layout detection is also enabled, its detections are layered on top. Layout detection is off by default, and Plain is the default output_format. The two are not currently linked: enabling layout detection while output_format stays Plain still runs the layout model, but its result is computed and then discarded rather than forcing a non-Plain output.

OutputFormat Description
Plain (default) Raw extracted text with minimal formatting
Markdown Markdown-formatted content
Djot Djot markup
Html HTML-formatted content
Json JSON tree with heading-driven sections
DocTags Docling DocTags tag-stream format (tables as OTSL). See DocTags Output
dot Graphviz DOT for a diagram recovered from a vector SVG or PDF source; empty string when no diagram was recovered. See Diagram DOT Output
Custom(name) Output from a renderer registered in the RendererRegistry (e.g. docx, latex)
config = ExtractionConfig(output_format="markdown")
result = extract("document.pdf", config=config)
print(result.content) # Markdown-formatted

Normalize extracted images to a uniform format after extraction but before post-processors.

By default, images are returned in their native format (JPEG from PDFs, PNG from rasterization, etc.). Set ImageExtractionConfig.output_format to re-encode all images to a single target format. This is useful for cloud pipelines that require uniform storage, thumbnails, or downstream processing.

Format Quality param Use case Notes
Native — Default; preserve source format No re-encode pass. Fastest.
Png — Lossless archival Large file sizes; recommended for quality-critical workflows.
Jpeg 1–100 Web/cloud storage Default quality 85. Lossy; good balance of size and quality.
Webp 1–100 Modern web use Default quality 80. Better compression than JPEG; requires browser support.
Heif 1–100 Apple ecosystem Default quality 80. Requires heic feature. Superior compression ratio vs JPEG/WebP.
Svg — Archival of vector images Lossless vector output. Raster sources return a warning; not auto-vectorized. Requires svg feature.

When the svg feature is active and output_format is set to Svg:

  • SVG → SVG: Re-parses the source via usvg and re-serializes. When svg.sanitize = true (default), strips <script> elements, external xlink:href/href attributes, <foreignObject> containers, and JavaScript event handlers. This is a lossy normalization for security.
  • SVG → PNG/JPEG/WebP/HEIF: Rasterizes to pixel format using resvg at the specified render_dpi (default 96.0, clamped 1.0–600.0 DPI).
  • Raster → SVG: Returns EncodeWarning::UnsupportedDirection; bytes are left untouched. No auto-vectorization.
  • Security: SVG input capped at 10 MB; rasterized output capped at 16384² pixels (~1 GB peak). External resource loading is disabled.
from xberg import ExtractionConfig, ImageExtractionConfig, ImageOutputFormat
# Normalize all images to WebP at quality 80
config = ExtractionConfig(
images=ImageExtractionConfig(
output_format=ImageOutputFormat.Webp(quality=80)
)
)

Enable or disable SVG security filtering:

from xberg import ExtractionConfig, ImageExtractionConfig, ImageOutputFormat, SvgOptions
# Re-encode SVG with sanitization disabled
config = ExtractionConfig(
images=ImageExtractionConfig(
output_format=ImageOutputFormat.Svg,
svg=SvgOptions(sanitize=False, render_dpi=96.0)
)
)

A flat array of typed elements (titles, paragraphs, tables, list items, code blocks, images, etc.). Each carries a page number; PDF text elements also carry bounding boxes when hierarchy extraction is enabled.

Use for RAG chunking, semantic search, or Unstructured.io-compatible pipelines.

Tests element-based result format with element type assertions on DOCX

Python
import asyncio
from xberg import extract, ExtractInput, ExtractInputKind
from xberg._xberg import ExtractionConfig
async def main() -> None:
input = ExtractInput(kind=ExtractInputKind("uri"), uri="https://example.com/docx/unit_test_headers.docx")
config = ExtractionConfig.from_json("{\"result_format\":\"element_based\"}")
result = await extract(input, config)
for element in result.results[0].elements or []:
print(element.element_type)
print(element.text)
asyncio.run(main())

Elements are in result.elements. Each element has element_id, element_type, text, and metadata.

element_type Description Key additional fields
title Main title or top-level heading level (h1–h6), font_size, font_name
heading Section/subsection heading level (h1–h6)
narrative_text Body paragraph —
list_item Bullet, numbered, or indented item list_type, list_marker, indent_level
table Tabular data row_count, column_count, format
image Embedded image format, width, height, alt_text
code_block Code snippet language, line_count
block_quote Quoted text —
header Recurring page header position
footer Recurring page footer position
page_break Page boundary marker next_page

Every element’s metadata contains:

Field Type Description
page_number int | None 1-indexed page number (PDF, DOCX, PPTX)
filename str | None Source filename
coordinates BoundingBox | None x0, y0, x1, y1 in PDF points. Only populated for text elements when pdf_options.hierarchy is enabled with include_bbox=True. Table and image elements do not carry coordinates.
element_index int Zero-based position in the elements array
additional dict[str, str] Element-type-specific fields (see table above)

PDF coordinates use bottom-left origin in points (1/72 inch).

{
"element_id": "elem-a3f2b1c4",
"element_type": "title",
"text": "Introduction to Machine Learning",
"metadata": {
"page_number": 1,
"element_index": 0,
"coordinates": { "x0": 72.0, "y0": 700.0, "x1": 540.0, "y1": 730.0 },
"additional": { "level": "h1", "font_size": "24" }
}
}
config = ExtractionConfig(result_format="element_based")
result = extract("document.pdf", config=config)
titles = [e for e in result.elements if e.element_type == "title"]
tables = [e for e in result.elements if e.element_type == "table"]
for title in titles:
level = title.metadata.additional.get("level", "h1")
print(f"[{level}] {title.text}")

If you’re migrating from Unstructured.io, element-based output follows a similar structure with these key differences:

Aspect Unstructured.io Xberg
Type names PascalCase (Title, NarrativeText) snake_case (title, narrative_text)
Element IDs Not always present Always present (deterministic hash)
Metadata Basic (page_number, filename) Extended (coordinates, additional fields)
Config key — result_format="element_based"

A flat list of nodes with explicit parent-child index references — a traversable tree with heading levels, content layers, inline annotations, and structured table grids.

Use when you need hierarchical relationships between sections.

Aspect Unified (default) Element-based Document structure
Output shape content: string elements: array nodes: array with index refs
Hierarchy None Inferred from levels Explicit parent/child indices
Inline annotations No No Bold, italic, links per node
Tables result.tables Table elements TableGrid with cell coords
Content layers Not classified Not classified body, header, footer, footnote
Best for LLM prompts, full-text RAG chunking Knowledge graphs, structured apps

Tests document structure with DOCX heading-driven nesting

Python
import asyncio
from xberg import extract, ExtractInput, ExtractInputKind
from xberg._xberg import ExtractionConfig
async def main() -> None:
input = ExtractInput(kind=ExtractInputKind("uri"), uri="https://example.com/docx/fake.docx")
config = ExtractionConfig.from_json("{\"include_document_structure\":true}")
result = await extract(input, config)
print(result.results[0].document)
asyncio.run(main())

Each node in result.document.nodes:

{
"id": "node-a3f2b1c4",
"content": { "node_type": "heading", "level": 2, "text": "Supervised Learning" },
"parent": 0,
"children": [4, 5, 6],
"content_layer": "body",
"page": 5,
"page_end": null,
"bbox": { "x0": 72.0, "y0": 600.0, "x1": 400.0, "y1": 620.0 },
"annotations": []
}
  • parent and children are integer indices into the nodes array (null if absent)
  • bbox is present when bounding box data is available
  • annotations contains inline formatting spans
node_type Key fields Notes
title text Document title
heading level (1–6), text Section heading
paragraph text Body paragraph; may have annotations
list ordered (bool) Container; children are list_item nodes
list_item text Child of list
table grid (TableGrid) Grid with cell-level data
image description, image_index image_index references result.images
code text, language Code block
quote (container) Children are typically paragraphs
formula text Math formula (plain text, LaTeX, or MathML)
footnote text Usually content_layer: "footnote"
group label, heading_level, heading_text Section grouping container
page_break (marker) Page boundary
Layer Description
body Main document content
header Page header area (repeated chapter titles)
footer Page footer area (page numbers, copyright)
footnote Footnotes and endnotes
for node in result.document["nodes"]:
if node["content_layer"] == "body":
process_main_content(node)

Paragraphs carry a list of annotations marking character spans:

{ "start": 0, "end": 16, "kind": { "annotation_type": "bold" } }
annotation_type Extra fields
bold, italic, underline, strikethrough —
code, subscript, superscript —
link url, title (optional)
for node in result.document["nodes"]:
for ann in node.get("annotations", []):
text = node["content"].get("text", "")
span = text[ann["start"]:ann["end"]]
kind = ann["kind"]["annotation_type"]
if kind == "link":
print(f"Link: {span} -> {ann['kind']['url']}")
else:
print(f"{kind}: {span}")

Table nodes contain a grid with cell-level data:

{
"rows": 3,
"cols": 3,
"cells": [
{ "content": "Algorithm", "row": 0, "col": 0, "row_span": 1, "col_span": 1, "is_header": true },
{
"content": "Decision Tree",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"is_header": false
}
]
}

Each cell has row, col, row_span, col_span, is_header, and optionally bbox.

for node in result.document["nodes"]:
if node["content"]["node_type"] == "table":
grid = node["content"]["grid"]
rows, cols = grid["rows"], grid["cols"]
table = [[None] * cols for _ in range(rows)]
for cell in grid["cells"]:
table[cell["row"]][cell["col"]] = cell["content"]
for row in table:
print(" | ".join(str(c or "") for c in row))

Classifies PDF text blocks into heading levels (H1–H6) and body text via K-means clustering on font sizes — largest cluster is H1, second-largest H2, and so on.

Python
import asyncio
from xberg import ExtractInput, extract, ExtractionConfig, PdfConfig, HierarchyConfig
async def main() -> None:
config: ExtractionConfig = ExtractionConfig(
pdf_options=PdfConfig(
extract_metadata=True,
hierarchy=HierarchyConfig(
enabled=True,
k_clusters=6,
include_bbox=True,
)
)
)
result = await extract(ExtractInput(uri="document.pdf"), config)
# Access hierarchy information
for page in result.results[0].pages or []:
print(f"Page {page.page_number}:")
print(f" Content: {page.content[:100]}...")
asyncio.run(main())

Hierarchy data is in result.pages[n].hierarchy. Each page has a blocks list:

{
"block_count": 4,
"blocks": [
{
"text": "Chapter 1: Introduction",
"level": "h1",
"font_size": 24.0,
"bbox": [50.0, 100.0, 400.0, 125.0]
},
{ "text": "Background", "level": "h2", "font_size": 18.0, "bbox": [50.0, 150.0, 300.0, 168.0] },
{
"text": "This chapter provides...",
"level": "body",
"font_size": 12.0,
"bbox": [50.0, 200.0, 550.0, 450.0]
}
]
}
  • bbox: [left, top, right, bottom] in PDF points (present when include_bbox=True). This is the only way to obtain bounding box coordinates for text elements — they are not included by default.
  • level: "h1" – "h6" or "body"
Parameter Type Default Description
enabled bool true Enable hierarchy extraction
k_clusters int 3 Font size clusters (1–7), maps to heading levels
include_bbox bool true Include bounding box coordinates
k_clusters Heading levels Use when
2–3 (default) H1–H2 Simple documents with 1–2 heading sizes
4–5 H1–H4 Standard documents
6–7 H1–H6+ Books, specs with deep nesting
  • hierarchy is None — Check hierarchy.enabled is True. This field is only populated by the native PDF text-tree pipeline; it stays None for OCR-backed (image-only) pages regardless of whether OCR is enabled — enabling OCR does not populate hierarchy. If fewer text blocks than k_clusters, reduce k_clusters. For structured output from a scanned PDF, use output_format (e.g. markdown) instead of relying on hierarchy.
  • Most blocks classified as body — Document may use uniform font sizes. Reduce k_clusters (try 3–4).
  • Heading levels don’t match visual inspection — Levels are assigned by font size rank, not absolute size. Filter on block.font_size directly for absolute thresholds.

See the HierarchyConfig reference for the full parameter list.