# Chandra Output Formats and Multi-Page Document Merging Explained

> Explore Chandra output formats like Markdown and HTML. Learn how Chandra merges multi-page documents, concatenates content, and aggregates statistics into a single metadata file.

- Repository: [Datalab/chandra](https://github.com/datalab-to/chandra)
- Tags: deep-dive
- Published: 2026-03-27

---

**Chandra generates Markdown, HTML, JSON metadata, and extracted images for each page, then merges multi-page documents by concatenating content with optional pagination delimiters while aggregating statistics into a single metadata file.**

The datalab-to/chandra repository provides an OCR pipeline that transforms documents into structured, machine-readable formats. Understanding Chandra output formats and the multi-page document merging process is essential for integrating the tool into automated document processing workflows.

## Per-Page Output Structure in Chandra

When the model finishes processing a page, the `InferenceManager.generate()` method returns a list of **`BatchOutputItem`** objects defined in [`chandra/model/schema.py`](https://github.com/datalab-to/chandra/blob/main/chandra/model/schema.py) (line 22). These data containers encapsulate every artefact extracted from a single page.

### The BatchOutputItem Schema

Each `BatchOutputItem` contains the following fields:

- **`markdown`** – Human-readable Markdown representation of the page layout, produced by `parse_markdown` in [`chandra/output.py`](https://github.com/datalab-to/chandra/blob/main/chandra/output.py).
- **`html`** – Fully-qualified HTML version of the page, generated by `parse_html`.
- **`chunks`** – A list of layout blocks containing bounding boxes, labels, and content extracted from the page.
- **`raw`** – The raw LLM-generated HTML response.
- **`page_box`** – The page’s bounding box coordinates in the original image.
- **`token_count`** – The number of LLM tokens consumed processing the page.
- **`images`** – A dictionary mapping generated image names (e.g., `abcd_1_img.webp`) to cropped `PIL.Image` objects for figures and tables.
- **`error`** – Boolean flag indicating whether the page failed to process.

These fields provide downstream flexibility, enabling consumption as plain text (Markdown), web previews (HTML), structured data (chunks), or visual assets (images).

## Multi-Page Document Merging Implementation

The CLI entry point in [`chandra/scripts/cli.py`](https://github.com/datalab-to/chandra/blob/main/chandra/scripts/cli.py) orchestrates per-page inference and invokes the `save_merged_output` function (lines 64-84) to collapse multiple pages into unified output files.

### Content Concatenation and Pagination

The merging logic iterates through results and appends content to accumulator lists:

```python
for page_num, result in enumerate(results):
    if page_num > 0 and paginate_output:
        all_markdown.append(f"\n\n{page_num}" + "-" * 48 + "\n\n")
        all_html.append(f"\n\n<!-- Page {page_num + 1} -->\n\n")
    
    all_markdown.append(result.markdown)
    all_html.append(result.html)

```

When the `--paginate_output` flag is enabled, the function inserts explicit delimiters between pages: a line of dashes for Markdown and HTML comments for HTML output.

### Metadata Aggregation and Image Handling

During the merge loop, the function accumulates global statistics and per-page metadata:

```python
total_tokens += result.token_count
total_chunks += len(result.chunks)
total_images += len(result.images)

page_metadata = {
    "page_num": page_num,
    "page_box": result.page_box,
    "token_count": result.token_count,
    "num_chunks": len(result.chunks),
    "num_images": len(result.images),
}
all_metadata.append(page_metadata)

```

Extracted images are persisted using deterministic names generated by `get_image_name` in [`chandra/output.py`](https://github.com/datalab-to/chandra/blob/main/chandra/output.py), following the pattern `{hash_html}_{div_idx}_img.webp`. If the user specifies `--save_images`, each `PIL.Image` is saved to the output directory while retaining the original cropping based on model-identified bounding boxes.

## Output File Structure

Running the CLI command `chandra input.pdf ./out` produces the following directory layout:

```

out/
└── input/
    ├── input.md           # Merged Markdown for all pages

    ├── input.html         # Merged HTML for all pages

    ├── input_metadata.json
    ├── 0_img.webp         # First extracted image

    ├── 1_img.webp         # Second extracted image

    └── …

```

The metadata JSON contains global totals (`total_tokens`, `total_chunks`, `total_images`) plus a per-page breakdown. You can suppress HTML generation with `--no-html` or skip image extraction with `--no-images`, though Markdown and metadata are always produced.

## Practical Usage Examples

Run the CLI on a PDF with explicit pagination:

```bash
chandra my_doc.pdf ./results --paginate_output

```

Process documents programmatically and merge manually:

```python
from chandra.input import load_file
from chandra.model import InferenceManager
from chandra.model.schema import BatchInputItem

pages = load_file("my_doc.pdf", {"page_range": None})
model = InferenceManager(method="vllm")
batch = [BatchInputItem(image=img, prompt_type="ocr_layout") for img in pages]
results = model.generate(batch, include_images=True)

merged_md = "\n\n".join(r.markdown for r in results)

```

## Summary

- Chandra outputs **Markdown**, **HTML**, **JSON chunks**, and **extracted images** per page via the `BatchOutputItem` class in [`chandra/model/schema.py`](https://github.com/datalab-to/chandra/blob/main/chandra/model/schema.py).
- The `save_merged_output` function in [`chandra/scripts/cli.py`](https://github.com/datalab-to/chandra/blob/main/chandra/scripts/cli.py) (lines 64-84) handles multi-page merging by concatenating content strings.
- Enable pagination with `--paginate_output` to insert visual delimiters between pages.
- Extracted images use deterministic naming via `get_image_name` and retain original bounding box cropping.
- Metadata aggregation provides both global statistics and detailed per-page breakdowns in a single JSON file.

## Frequently Asked Questions

### What output formats does Chandra generate?

Chandra produces Markdown for plain-text pipelines, fully-qualified HTML for web previews, JSON-serializable chunks containing bounding boxes and layout labels, and extracted images in WebP format. Each format serves different downstream processing needs, from human-readable documents to structured data extraction.

### How does Chandra merge multi-page documents?

The merging logic resides in `save_merged_output` within [`chandra/scripts/cli.py`](https://github.com/datalab-to/chandra/blob/main/chandra/scripts/cli.py), which concatenates individual page Markdown and HTML strings while aggregating token counts, chunk counts, and image statistics into a single metadata JSON file. When the `--paginate_output` flag is enabled, the function inserts explicit page separators using dashed lines in Markdown and HTML comments in HTML.

### Can I disable specific output formats when running Chandra?

Yes. The CLI supports flags such as `--no-html` and `--no-images` to suppress generation of specific formats. However, Markdown and metadata files are always produced regardless of these flags, ensuring baseline output for every document processed.

### Where does Chandra store extracted images and how are they named?

Extracted images are saved in the output directory using deterministic names generated by `get_image_name` in [`chandra/output.py`](https://github.com/datalab-to/chandra/blob/main/chandra/output.py), following the pattern `{hash_html}_{div_idx}_img.webp`. The images retain their original cropping based on model-identified bounding boxes from the source document.