Skip to content

Headers ​

Two strategies decide which spans become Markdown headings.

IdentifyHeaders (default) ​

Font-size frequency: counts how many characters appear at each rounded font size, picks the most common size as the body limit, then assigns # … ###### to the larger sizes in descending order.

ts
import { toMarkdown, IdentifyHeaders } from "@nalinor/mupdf4llm";

const md = await toMarkdown(buf); // implicit IdentifyHeaders

Tune it manually:

ts
import * as mupdf from "mupdf";
import { toMarkdown, IdentifyHeaders } from "@nalinor/mupdf4llm";

const doc = mupdf.PDFDocument.openDocument(buf, "application/pdf");
const hdr = new IdentifyHeaders(doc, {
  body_limit: 11, // body text size in pt (inclusive)
  max_levels: 4, // cap at H4
  pages: [0, 1, 2], // analyze only the first three pages
});

const md = await toMarkdown(buf, { hdrInfo: hdr });

TocHeaders ​

Uses the PDF outline (table of contents). Each TOC entry maps a title + level to a specific page; on that page the first span whose trimmed text matches the title gets that level's #. Useful when font sizes don't reliably encode structure (e.g. reports with a flat type hierarchy but an authored outline).

ts
import { toMarkdown, TocHeaders } from "@nalinor/mupdf4llm";

const md = await toMarkdown(buf, { hdrInfo: new TocHeaders(doc) });

Custom provider ​

Anything with a get_header_id(span, pageCtx?): string method works:

ts
const hdr = {
  get_header_id(span) {
    return span.font.includes("Heading-1") ? "# " : "";
  },
};
await toMarkdown(buf, { hdrInfo: hdr });

Disabling ​

Pass false to skip header inference entirely (everything becomes body text):

ts
await toMarkdown(buf, { hdrInfo: false });

AGPL-3.0-or-later — inherited from PyMuPDF and pymupdf4llm.