Output Formats
Choose the format that matches your downstream processing. See the Configuration Reference for all output format options.
Available Formats
Section titled “Available Formats”- Content Format —
output_formatcontrols thecontentfield: Plain, Markdown, Djot, HTML, JSON, Structured, DocTags, DOT, or a custom renderer - 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
Unified Output (Default)
Section titled “Unified Output (Default)”No configuration required. The result contains:
content— Full document text with minimal formattingpages— Per-page breakdown for PDFs, DOCX, and PPTXtables— Extracted tables in structured formatimages— Image metadata and paths
Table Identity and Anchors
Section titled “Table Identity and Anchors”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 singletables[]entry before ids are assigned, so in practicetable_idis 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.Nonewhen the extractor did not assign one.columns(list[str] | None) — the header cells (the first row ofcells), 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 needingtable_idto look up siblings.Nonewhen 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.
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)use xberg::{extract, ExtractInput, ExtractionConfig, OutputFormat};
let config = ExtractionConfig { output_format: OutputFormat::Markdown, table_anchors: true, ..Default::default()};let output = extract(ExtractInput::from_uri("report.pdf"), &config).await?;let result = &output.results[0];for table in &result.tables { println!("{:?} {:?}", table.table_id, table.columns);}output_format = "markdown"table_anchors = trueContent Output Format
Section titled “Content Output Format”output_format sets the format of the content field. It is independent of result_format (element-based / document structure) and applies to every extraction.
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 |
Structured |
Currently identical to Plain text (no OCR element data, bounding boxes, or confidence scores); only the metadata.output_format label differs. HTML sources are routed through Markdown conversion first, so they come out as Markdown-flavored text |
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-formattedImage Output Formats
Section titled “Image Output Formats”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.
Supported Formats
Section titled “Supported Formats”| 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 | (v5+) Lossless vector output. Raster sources return a warning; not auto-vectorized. Requires svg feature. |
SVG Support (v5+)
Section titled “SVG Support (v5+)”When the svg feature is active and output_format is set to Svg:
- SVG → SVG: Re-parses the source via
usvgand re-serializes. Whensvg.sanitize = true(default), strips<script>elements, externalxlink:href/hrefattributes,<foreignObject>containers, and JavaScript event handlers. This is a lossy normalization for security. - SVG → PNG/JPEG/WebP/HEIF: Rasterizes to pixel format using
resvgat the specifiedrender_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.
Configuration
Section titled “Configuration”from xberg import ExtractionConfig, ImageExtractionConfig, ImageOutputFormat
# Normalize all images to WebP at quality 80config = ExtractionConfig( images=ImageExtractionConfig( output_format=ImageOutputFormat.Webp(quality=80) ))import { ExtractionConfig, ImageOutputFormat } from "@xberg-io/xberg";
const config: ExtractionConfig = { images: { outputFormat: { type: "webp", quality: 80 } }};use xberg::{ExtractionConfig, ImageExtractionConfig, ImageOutputFormat};
let config = ExtractionConfig { images: Some(ImageExtractionConfig { output_format: ImageOutputFormat::Webp { quality: 80 }, ..Default::default() }), ..Default::default()};SVG Sanitization (v5+)
Section titled “SVG Sanitization (v5+)”Enable or disable SVG security filtering:
from xberg import ExtractionConfig, ImageExtractionConfig, ImageOutputFormat, SvgOptions
# Re-encode SVG with sanitization disabledconfig = ExtractionConfig( images=ImageExtractionConfig( output_format=ImageOutputFormat.Svg, svg=SvgOptions(sanitize=False, render_dpi=96.0) ))use xberg::{ExtractionConfig, ImageExtractionConfig, ImageOutputFormat, SvgOptions};
let config = ExtractionConfig { images: Some(ImageExtractionConfig { output_format: ImageOutputFormat::Svg, svg: Some(SvgOptions { sanitize: false, render_dpi: 96.0 }), ..Default::default() }), ..Default::default()};Element-Based Output
Section titled “Element-Based Output”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.
Enable
Section titled “Enable”import asynciofrom xberg import ExtractInput, extract, ExtractionConfig, ElementType
async def main() -> None: # Configure element-based output config = ExtractionConfig(result_format="element_based")
# Extract document result = await extract(ExtractInput(uri="document.pdf"), config) elements = result.results[0].elements or []
# Access elements for element in elements: print(f"Type: {element.element_type}") print(f"Text: {element.text[:100]}")
if element.metadata.page_number: print(f"Page: {element.metadata.page_number}")
if element.metadata.coordinates: coords = element.metadata.coordinates print(f"Coords: ({coords.x0}, {coords.y0}) - ({coords.x1}, {coords.y1})")
print("---")
# Filter by element type titles = [e for e in elements if e.element_type == ElementType.TITLE] for title in titles: level = title.metadata.additional.get("level", "unknown") print(f"[{level}] {title.text}")
asyncio.run(main())import { extract, ElementType, type ExtractionConfig } from "@xberg-io/xberg";
// Configure element-based outputconst config: ExtractionConfig = { resultFormat: "element_based",};
// Extract documentconst output = await extract({ kind: "uri", uri: "document.pdf" }, config);const result = output.results[0];
// Access elementsfor (const element of result.elements ?? []) { console.log(`Type: ${element.elementType}`); console.log(`Text: ${element.text.slice(0, 100)}`);
if (element.metadata.pageNumber) { console.log(`Page: ${element.metadata.pageNumber}`); }
if (element.metadata.coordinates) { const coords = element.metadata.coordinates; console.log(`Coords: (${coords.x0}, ${coords.y0}) - (${coords.x1}, ${coords.y1})`); }
console.log("---");}
// Filter by element typeconst titles = (result.elements ?? []).filter((e) => e.elementType === ElementType.Title);for (const title of titles) { const level = title.metadata.additional?.level || "unknown"; console.log(`[${level}] ${title.text}`);}use xberg::{extract, ExtractionConfig, ExtractInput};use xberg::types::OutputFormat as ResultFormat;
#[tokio::main]async fn main() -> xberg::Result<()> { // Configure element-based output (result_format controls Unified vs ElementBased) let config = ExtractionConfig { result_format: ResultFormat::ElementBased, ..Default::default() };
// Extract document let output = extract(ExtractInput::from_uri("document.pdf"), &config).await?; let result = &output.results[0];
// Access elements if let Some(elements) = &result.elements { for element in elements { println!("Type: {:?}", element.element_type); println!("Text: {}", &element.text[..100.min(element.text.len())]);
if let Some(page) = element.metadata.page_number { println!("Page: {}", page); }
if let Some(coords) = &element.metadata.coordinates { println!("Coords: ({}, {}) - ({}, {})", coords.x0, coords.y0, coords.x1, coords.y1); }
println!("---"); }
// Filter by element type let titles: Vec<_> = elements.iter() .filter(|e| matches!(e.element_type, xberg::types::ElementType::Title)) .collect();
for title in titles { let level = title.metadata.additional.get("level") .map(|v| v.as_ref()) .unwrap_or("unknown"); println!("[{}] {}", level, title.text); } }
Ok(())}package main
import ( "fmt" "xberg")
func main() { // Configure element-based output cfg := xberg.ExtractionConfig{ OutputFormat: "element_based", }
// Extract document input := xberg.ExtractInputFromURI("document.pdf") result, err := xberg.Extract(*input, cfg) if err != nil { panic(err) }
// Access elements for _, element := range result.Results[0].Elements { fmt.Printf("Type: %s\n", element.ElementType)
text := element.Text if len(text) > 100 { text = text[:100] } fmt.Printf("Text: %s\n", text)
if element.Metadata.PageNumber != nil { fmt.Printf("Page: %d\n", *element.Metadata.PageNumber) }
if element.Metadata.Coordinates != nil { coords := element.Metadata.Coordinates fmt.Printf("Coords: (%f, %f) - (%f, %f)\n", coords.Left, coords.Top, coords.Right, coords.Bottom) }
fmt.Println("---") }
// Filter by element type var titles []xberg.Element for _, element := range result.Results[0].Elements { if element.ElementType == "title" { titles = append(titles, element) } }
for _, title := range titles { level, ok := title.Metadata.Additional["level"].(string) if !ok { level = "unknown" } fmt.Printf("[%s] %s\n", level, title.Text) }}require 'xberg'
# Configure element-based outputconfig = Xberg::ExtractionConfig.new(output_format: 'element_based')
# Extract documentinput = Xberg::ExtractInput.new(uri: 'document.pdf')result = Xberg.extract(input, config)
# Access elementsresult.results.first.elements.each do |element| puts "Type: #{element.element_type}" puts "Text: #{element.text[0...100]}"
puts "Page: #{element.metadata.page_number}" if element.metadata.page_number
if element.metadata.coordinates coords = element.metadata.coordinates puts "Coords: (#{coords.left}, #{coords.top}) - (#{coords.right}, #{coords.bottom})" end
puts "---"end
# Filter by element typetitles = result.results.first.elements.select { |e| e.element_type == 'title' }titles.each do |title| level = title.metadata.additional['level'] || 'unknown' puts "[#{level}] #{title.text}"end<?phpuse Xberg\ExtractionConfig;use Xberg\XbergApi;use Xberg\Xberg;
// Configure element-based output$config = ExtractionConfig::from_json(json_encode([ 'resultFormat' => 'element_based',]));
// Extract document$resultOutput = Xberg::extract(\Xberg\ExtractInput::fromUri('document.pdf'), $config);$result = $resultOutput->getResults()[0];
// Access elementsforeach ($result->getElements() as $element) { echo "Type: " . $element->getElementType() . "\n"; echo "Text: " . substr($element->getText(), 0, 100) . "\n";
if ($element->metadata->pageNumber) { echo "Page: " . $element->metadata->pageNumber . "\n"; }
if ($element->metadata->coordinates) { $coords = $element->metadata->coordinates; echo sprintf("Coords: (%s, %s) - (%s, %s)\n", $coords->getLeft(), $coords->getTop(), $coords->getRight(), $coords->getBottom()); }
echo "---\n";}
// Filter by element type$titles = array_filter($result->getElements(), function($e) { return $e->getElementType() === 'title';});
foreach ($titles as $title) { $level = $title->metadata->additional['level'] ?? 'unknown'; echo "[{$level}] {$title->getText()}\n";}?>Elements are in result.elements. Each element has element_id, element_type, text, and metadata.
Element Types
Section titled “Element Types”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 |
Metadata
Section titled “Metadata”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).
Example Output
Section titled “Example Output”{ "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" } }}Filtering Elements
Section titled “Filtering Elements”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}")Migrating from Unstructured.io
Section titled “Migrating from Unstructured.io”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" |
Document Structure
Section titled “Document Structure”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.
Comparison
Section titled “Comparison”| 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 |
Enable
Section titled “Enable”import asynciofrom xberg import ExtractInput, extract, ExtractionConfig
async def main() -> None: # Enable document structure output config = ExtractionConfig(include_document_structure=True)
result = await extract(ExtractInput(uri="document.pdf"), config)
# Access the document tree document = result.results[0].document if document: for node in document.nodes: node_type = node.content.node_type text = getattr(node.content, "text", "") print(f"[{node_type}] {text[:80]}")
asyncio.run(main())import { extract, type ExtractionConfig } from "@xberg-io/xberg";
const config: ExtractionConfig = { includeDocumentStructure: true,};
const output = await extract({ kind: "uri", uri: "document.pdf" }, config);const result = output.results[0];
if (result.document) { for (const node of result.document.nodes ?? []) { console.log(`[${node.content.node_type}] ${"text" in node.content ? node.content.text : ""}`); }}use xberg::{extract, ExtractionConfig, ExtractInput};
let config = ExtractionConfig { include_document_structure: true, ..Default::default()};
let output = extract(ExtractInput::from_uri("document.pdf"), &config).await?;let result = &output.results[0];
if let Some(document) = &result.document { for node in &document.nodes { let text = node.content.text().unwrap_or(""); println!("[{}] {}", node.content.node_type_str(), &text[..text.len().min(80)]); }}package main
import ( "fmt" xberg "github.com/xberg-io/xberg/packages/go")
func main() { config := xberg.NewExtractionConfig( xberg.WithIncludeDocumentStructure(true), )
input := xberg.ExtractInputFromURI("document.pdf") result, err := xberg.Extract(*input, *config) if err != nil { panic(err) }
if result.Results[0].Document != nil { for _, node := range result.Results[0].Document.Nodes { fmt.Printf("[%s]\n", node.Content.NodeType) } }}import io.xberg.Xberg;import io.xberg.ExtractInputKind;import io.xberg.ExtractionConfig;import io.xberg.ExtractedDocument;
ExtractionConfig config = ExtractionConfig.builder() .includeDocumentStructure(true) .build();var resultOutput = Xberg.extract( io.xberg.ExtractInput.builder() .withKind(io.xberg.ExtractInputKind.Uri) .withUri("document.pdf") .build(), config);ExtractedDocument result = resultOutput.results().get(0);if (result.getDocumentStructure().isPresent()) { var document = result.getDocumentStructure().get(); for (var node : document.nodes()) { System.out.println("[" + node.content().nodeType() + "]"); }}using Xberg;
var config = new ExtractionConfig{ IncludeDocumentStructure = true};
var result = (await XbergConverter.ExtractAsync(ExtractInput.FromUri("document.pdf"), config)).Results[0];
if (result.Document is not null){ foreach (var node in result.Document.Nodes) { Console.WriteLine($"[{node.Content.NodeType}]"); }}require 'xberg'
config = Xberg::ExtractionConfig.new(include_document_structure: true)
input = Xberg::ExtractInput.new(uri: 'document.pdf')result = Xberg.extract(input, config)
if result.results.first.document result.results.first.document['nodes'].each do |node| node_type = node['content']['node_type'] text = node['content']['text'] || '' puts "[#{node_type}] #{text[0...80]}" endendNode Shape
Section titled “Node Shape”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": []}parentandchildrenare integer indices into thenodesarray (nullif absent)bboxis present when bounding box data is availableannotationscontains inline formatting spans
Node Types
Section titled “Node Types”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 |
Content Layers
Section titled “Content Layers”| 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)Text Annotations
Section titled “Text Annotations”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 Grid
Section titled “Table Grid”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))PDF Hierarchy Detection
Section titled “PDF Hierarchy Detection”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.
Quick Start
Section titled “Quick Start”import asynciofrom 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())import { extract } from "@xberg-io/xberg";
const config = { pdfOptions: { extractMetadata: true, hierarchy: { enabled: true, kClusters: 6, includeBbox: true, ocrCoverageThreshold: 0.8, }, },};
const output = await extract({ kind: "uri", uri: "document.pdf" }, config);const result = output.results[0];if (result.pages) { result.pages.forEach((page) => { console.log(`Page ${page.pageNumber}:`); console.log(` Content: ${page.content.substring(0, 100)}...`); });}use xberg::{extract, ExtractionConfig, ExtractInput, PdfConfig, HierarchyConfig};
#[tokio::main]async fn main() -> xberg::Result<()> { let config = ExtractionConfig { pdf_options: Some(PdfConfig { hierarchy: Some(HierarchyConfig { enabled: true, detection_threshold: Some(0.75), ocr_coverage_threshold: Some(0.8), min_level: Some(1), max_level: Some(5), }), ..Default::default() }), ..Default::default() };
let output = extract(ExtractInput::from_uri("document.pdf"), &config).await?; println!("Hierarchy levels: {}", output.results[0].hierarchy.len()); Ok(())}package main
import "github.com/xberg-io/xberg/packages/go"
func main() { enabled := true includeBbox := true kClusters := uint(6) kClustersAdvanced := uint(12) threshold := float32(0.8)
// Basic hierarchy configuration config := xberg.ExtractionConfig{ PdfOptions: &xberg.PdfConfig{ ExtractImages: true, Hierarchy: &xberg.HierarchyConfig{ Enabled: &enabled, KClusters: &kClusters, IncludeBbox: &includeBbox, OcrCoverageThreshold: &threshold, }, }, }
// Advanced hierarchy configuration with more clusters advancedConfig := xberg.ExtractionConfig{ PdfOptions: &xberg.PdfConfig{ ExtractImages: true, Hierarchy: &xberg.HierarchyConfig{ Enabled: &enabled, KClusters: &kClustersAdvanced, IncludeBbox: &includeBbox, OcrCoverageThreshold: &threshold, }, }, }
_ = config _ = advancedConfig}import io.xberg.ExtractionConfig;import io.xberg.PdfConfig;import io.xberg.HierarchyConfig;
ExtractionConfig config = ExtractionConfig.builder() .pdfOptions(PdfConfig.builder() .hierarchyConfig(HierarchyConfig.builder() .enabled(true) .detectionThreshold(0.75) .ocrCoverageThreshold(0.8) .minLevel(1) .maxLevel(5) .build()) .build()) .build();using Xberg;
// Basic hierarchy configuration with propertiesvar config = new ExtractionConfig{ PdfOptions = new PdfConfig { ExtractImages = true, Hierarchy = new HierarchyConfig { Enabled = true, KClusters = 6, IncludeBbox = true, OcrCoverageThreshold = 0.8f } }};
var result = (await XbergConverter.ExtractAsync(ExtractInput.FromUri("document.pdf"), config)).Results[0];Console.WriteLine($"Content length: {result.Content.Length}");
// Advanced hierarchy detection with custom parametersvar advancedConfig = new ExtractionConfig{ PdfOptions = new PdfConfig { ExtractImages = true, Hierarchy = new HierarchyConfig { Enabled = true, KClusters = 12, // More clusters for detailed hierarchy IncludeBbox = true, // Include bounding box coordinates OcrCoverageThreshold = 0.7f // Higher OCR threshold for stricter detection } }};
var result = (await XbergConverter.ExtractAsync(ExtractInput.FromUri("complex_document.pdf"), advancedConfig)).Results[0];Console.WriteLine($"Advanced hierarchy detection completed: {result.Content.Length} chars");
// Minimal configuration with only enabled flagvar minimalConfig = new ExtractionConfig{ PdfOptions = new PdfConfig { Hierarchy = new HierarchyConfig { Enabled = true, // Other properties use defaults: // KClusters = 6 // IncludeBbox = true } }};
var result = (await XbergConverter.ExtractAsync(ExtractInput.FromUri("document.pdf"), minimalConfig)).Results[0];Console.WriteLine("Extraction with default hierarchy settings complete");
// Disabling hierarchy detectionvar noHierarchyConfig = new ExtractionConfig{ PdfOptions = new PdfConfig { Hierarchy = new HierarchyConfig { Enabled = false } }};
var result = (await XbergConverter.ExtractAsync(ExtractInput.FromUri("document.pdf"), noHierarchyConfig)).Results[0];Console.WriteLine("Extraction without hierarchy detection complete");require 'xberg'
# Using keyword arguments with defaultsconfig = Xberg::ExtractionConfig.new( pdf_options: Xberg::PdfConfig.new( extract_images: true, hierarchy: Xberg::HierarchyConfig.new( enabled: true, k_clusters: 6, include_bbox: true, ocr_coverage_threshold: 0.8 ) ))
# Using hash syntax alternativeconfig = Xberg::ExtractionConfig.new( pdf_options: Xberg::PdfConfig.new( extract_images: true, hierarchy: { enabled: true, k_clusters: 6, include_bbox: true, ocr_coverage_threshold: 0.8 } ))Output
Section titled “Output”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 wheninclude_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"
Configuration
Section titled “Configuration”| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
true |
Enable hierarchy extraction |
k_clusters |
int |
3 |
Font size clusters (2–10), maps to heading levels |
include_bbox |
bool |
true |
Include bounding box coordinates |
ocr_coverage_threshold |
float | None |
None |
Trigger OCR if text coverage is below this fraction |
Choosing k_clusters
Section titled “Choosing k_clusters”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 | H1–H6 | Most documents |
| 7–8 | H1–H6+ | Books, specs with deep nesting |
Ocr_coverage_threshold
Section titled “Ocr_coverage_threshold”| Threshold | Behavior |
|---|---|
None |
OCR never triggered by coverage |
0.3 |
OCR if < 30% of page has text |
0.5 |
OCR if < 50% of page has text |
Requires an OCR backend to be configured separately.
Troubleshooting
Section titled “Troubleshooting”hierarchyisNone— Checkhierarchy.enabledisTrue. If the PDF is image-only, enable OCR. If fewer text blocks thank_clusters, reducek_clusters.- Most blocks classified as
body— Document may use uniform font sizes. Reducek_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_sizedirectly for absolute thresholds.
See the HierarchyConfig reference for the full parameter list.