Skip to main content
Lesson 7 of 16 · Document AI: OCR and Document Intelligence

Headers, footers, and pagination

Lesson 7 of 16 Updated November 2, 2026 Developer
On this page
  1. Separate decoration from content
  2. Accessing the fields
  3. Reconstructing pagination
  4. Clean content for RAG
  5. Leverage footer metadata
  6. Process only part of the document
  7. Key takeaways

Updated November 2, 2026 — OCR. OCR 4.0 (mistral-ocr-4-0) was retired on September 30, 2026. Use OCR 4.1 (mistral-ocr-4-1), which has been generally available since August 31 at the same price, or the alias mistral-ocr-latest, which has pointed to it since July 16. OCR 4.1 adds confidence scores at the block level.

Separate decoration from content

Since OCR version 2512 (December 2025), Mistral’s OCR model extracts headers and footers separately for each page. This feature might seem trivial until you index a 120-page contract: the law firm’s name, the “Confidential” marking, and the page numbers appear 120 times, polluting every search result. On formal documents—contracts, annual reports, regulatory texts—this separation transforms the quality of everything that follows.

The dilemma is real. These elements repeat mechanically on every page, creating noise in both indexing and RAG system responses. Yet they sometimes contain the document’s only reliable metadata: version number, issue date, classification level. Extracting them separately, rather than deleting them or leaving them in the body text, lets you decide case by case.

Accessing the fields

The header and footer fields are available directly on each page of the response:

import os
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

ocr_response = client.ocr.process(
    model="mistral-ocr-latest",
    document={
        "type": "document_url",
        "document_url": "https://exemple.com/contrat.pdf"
    }
)

for page in ocr_response.pages:
    print(f"--- Page {page.index + 1} ---")

    if page.header:
        print(f"Header: {page.header}")

    print(f"Content: {page.markdown[:100]}...")

    if page.footer:
        print(f"Footer: {page.footer}")

    print()

The if page.header checks aren’t superfluous: not every page has a header, starting with cover pages, in which case the field is None.

Reconstructing pagination

Each page is zero-indexed. For a multi-page document you plan to store or return to an application, it’s better to normalize the structure immediately: human-readable number, content, decoration, dimensions, and image count.

def extract_with_pagination(ocr_response) -> dict:
    """Extract content while preserving pagination."""
    document = {
        "page_count": len(ocr_response.pages),
        "pages": []
    }

    for page in ocr_response.pages:
        page_data = {
            "number": page.index + 1,
            "content": page.markdown,
            "header": page.header or "",
            "footer": page.footer or "",
            "dimensions": {
                "width": page.dimensions.width if page.dimensions else None,
                "height": page.dimensions.height if page.dimensions else None,
            },
            "image_count": len(page.images) if page.images else 0,
        }
        document["pages"].append(page_data)

    return document

The or "" operators and guards on page.dimensions convert potentially null values into neutral defaults. This prevents an AttributeError halfway through a batch when processing the 80th document.

Dimensions are expressed in PDF points, where one point equals 1/72 of an inch. The formats you’ll encounter most often:

  • A4: 595 x 842 points
  • Letter: 612 x 792 points
  • A3: 842 x 1191 points

This information helps you spot outlier pages in a batch—an A3 blueprint slipped among A4 sheets, or a scan at an unusual scale. Converting to centimeters makes diagnosis immediate:

for page in ocr_response.pages:
    if page.dimensions:
        width_cm = page.dimensions.width * 2.54 / 72
        height_cm = page.dimensions.height * 2.54 / 72
        print(f"Page {page.index + 1}: {width_cm:.1f} x {height_cm:.1f} cm")

Clean content for RAG

Depending on the document, the header and footer may also appear in the main markdown. Before indexing, remove them when they actually frame the content:

def main_content(ocr_response) -> str:
    """Extract only the main content, without headers/footers."""
    parts = []

    for page in ocr_response.pages:
        content = page.markdown

        # Optional: remove headers/footers from markdown
        # if they are included in the main content
        if page.header and content.startswith(page.header):
            content = content[len(page.header):].strip()

        if page.footer and content.endswith(page.footer):
            content = content[:-len(page.footer)].strip()

        parts.append(content)

    return "\n\n".join(parts)

The startswith and endswith conditions are intentionally cautious: we only remove the string if it truly appears at the start or end of the page. Blind removal via replace risks erasing a legitimate occurrence of the company name in the middle of the text.

Once footers are isolated, a few regular expressions suffice to extract structured metadata: pagination in “3 / 40” format, issue date, version number, and classification level.

import re

def extract_footer_metadata(ocr_response) -> dict:
    """Extract metadata from footers."""
    metadata = {
        "page_numbers": [],
        "dates": [],
        "versions": [],
        "classifications": [],
    }

    for page in ocr_response.pages:
        footer = page.footer or ""

        # Page number search
        match_page = re.search(r"(\d+)\s*/\s*(\d+)", footer)
        if match_page:
            metadata["page_numbers"].append(
                f"{match_page.group(1)}/{match_page.group(2)}"
            )

        # Date search
        match_date = re.search(r"\d{2}/\d{2}/\d{4}", footer)
        if match_date:
            metadata["dates"].append(match_date.group())

        # Version search
        match_version = re.search(r"v(?:ersion)?\s*(\d+\.?\d*)", footer, re.I)
        if match_version:
            metadata["versions"].append(match_version.group(1))

        # Classification (Confidential, Public, etc.)
        for classification in ["confidential", "public", "internal", "secret"]:
            if classification in footer.lower():
                metadata["classifications"].append(classification)

    return metadata

The value goes beyond documentary curiosity. The classification detected here can drive downstream processing: route items marked “confidential” to a restricted-access index, or refuse to send them to an external service. Obviously, adapt the vocabulary to match your organization’s terminology.

Process only part of the document

For large documents, you often need only a section—the financial appendices of a report, the signed chapter of a contract. Filtering happens after extraction, on the response you’ve already received:

def extract_range(ocr_response, start: int, end: int) -> str:
    """Extract content from a page range (1-indexed)."""
    parts = []

    for page in ocr_response.pages:
        number = page.index + 1
        if start <= number <= end:
            parts.append(page.markdown)

    return "\n\n---\n\n".join(parts)


# Extract pages 5 to 10
partial_content = extract_range(ocr_response, 5, 10)

The function uses human-readable numbering, starting from 1: requesting pages 5 to 10 returns exactly those pages. Remember, however, that the entire document was processed—and billed—before this filtering. If you know in advance that only a few pages interest you, split the PDF upstream rather than sending all 1,000 pages to the API.

Key takeaways

  • Headers and footers are extracted separately since OCR 2512
  • Filter them to avoid noise in your RAG pipelines
  • Page dimensions are in PDF points (72 points = 1 inch)
  • Footers often contain useful metadata (version, date, classification)
  • Process by page ranges for large documents