This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/pdfjs-document-outline-bookmarks-metadata.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. PDF.js: Access outline, bookmarks, and metadata

Table of contents

    PDF documents contain structural metadata beyond the visible content — table of contents, bookmarks, file attachments, and document properties. This tutorial shows how PDF.js exposes all of these through the PDFDocumentProxy API.
    PDF.js: Access outline, bookmarks, and metadata
    TL;DR
    • Pull the outline/bookmark tree with pdfDocument.getOutline() and navigate via linkService.goToDestination().
    • Read PDF metadata (title, author, version, form/signature flags) with pdfDocument.getMetadata() and Extensible Metadata Platform (XMP) via the returned metadata field.
    • Access embedded files with getAttachments() and toggle layer visibility with getOptionalContentConfig().

    Prerequisites

    This guide assumes you have a loaded PDFDocumentProxy (the value getDocument(...).promise resolves to) and, for the navigation examples, a configured PDFLinkService. If you haven’t wired those up yet, start with our blog on how to set up a custom PDF.js viewer in React.

    You’ll also need:

    • pdfjs-dist 4.x or later
    • A place to render a tree of React components alongside the page canvases (for the sidebar UI section)

    Document outline (table of contents)

    The outline is the PDF’s bookmark tree — the table of contents that PDF readers show in a sidebar:

    const outline = await pdfDocument.getOutline();

    Outline structure

    Each outline item has:

    {
    title: "Chapter 1: Introduction", // Display text.
    bold: false, // Bold styling.
    italic: false, // Italic styling.
    color: Uint8ClampedArray(3), // RGB color [r, g, b].
    dest: [...], // Internal destination (page + position).
    url: null, // External URL (if it links outside).
    unsafeUrl: null, // Unsanitized version of `url`, before PDF.js escapes it.
    newWindow: false, // Whether an external URL should open in a new window.
    count: 3, // Number of child items.
    items: [...] // Nested child outline items.
    }

    Rendering an outline sidebar

    This component fetches the outline when the document loads and renders it as a nested list, navigating to a destination or opening an external URL when a reader clicks an item:

    function OutlineSidebar({ pdfDocument, linkService }) {
    const [outline, setOutline] = useState(null);
    useEffect(() => {
    pdfDocument.getOutline().then(setOutline);
    }, [pdfDocument]);
    if (!outline) return <div>No outline available</div>;
    return <OutlineTree items={outline} linkService={linkService} />;
    }
    function OutlineTree({ items, linkService, depth = 0 }) {
    return (
    <ul style={{ paddingLeft: depth * 16 }}>
    {items.map((item, i) => (
    <li key={i}>
    <button
    onClick={() => {
    if (item.dest) {
    linkService.goToDestination(item.dest);
    } else if (item.url) {
    window.open(item.url, "_blank");
    }
    }}
    style={{
    fontWeight: item.bold ? "bold" : "normal",
    fontStyle: item.italic ? "italic" : "normal",
    }}
    >
    {item.title}
    </button>
    {item.items?.length > 0 && (
    <OutlineTree
    items={item.items}
    linkService={linkService}
    depth={depth + 1}
    />
    )}
    </li>
    ))}
    </ul>
    );
    }

    Outline items contain dest arrays that you pass directly to linkService.goToDestination():

    // Named destination (string).
    linkService.goToDestination("chapter-1");
    // Explicit destination (array).
    linkService.goToDestination([
    pageRef, // Page reference object { num, gen } from `getDestination()`.
    // — Convert with `pdfDocument.getPageIndex(pageRef)` if you need an index.
    { name: "XYZ" }, // Type: XYZ, Fit, FitH, FitV, FitR, FitB.
    x, // x position (or null).
    y, // y position (or null).
    zoom, // Zoom level (or null).
    ]);

    Resolving named destinations

    Named destinations are strings that resolve to an explicit destination array. Look one up directly, or list every named destination the document defines:

    // Resolve a named destination to an explicit destination array.
    const dest = await pdfDocument.getDestination("chapter-1");
    // Returns: [pageRef, { name: "XYZ" }, x, y, zoom].
    // Get all named destinations.
    const destinations = await pdfDocument.getDestinations();

    Document metadata

    PDF.js exposes document properties and XMP metadata through a single call:

    const metadata = await pdfDocument.getMetadata();

    Metadata structure

    The returned object separates the legacy Document Information Dictionary from XMP metadata and a few document-level flags:

    {
    info: {
    Title: "My Document",
    Author: "John Doe",
    Subject: "PDF Guide",
    Keywords: "pdf, javascript",
    Creator: "LaTeX",
    Producer: "pdfTeX-1.40.25",
    CreationDate: "D:20240115120000Z",
    ModDate: "D:20240115120000Z",
    PDFFormatVersion: "1.7",
    IsLinearized: false,
    IsAcroFormPresent: false,
    IsXFAPresent: false,
    IsCollectionPresent: false,
    IsSignaturesPresent: false,
    },
    metadata: {
    // XMP metadata (XML-based, may contain additional fields).
    // Access via `metadata.get("dc:title")`, `metadata.get("dc:creator")`, etc.
    },
    contentDispositionFilename: null,
    contentLength: 1234567,
    hasStructTree: true, // True if the PDF has an accessibility structure tree.
    }

    Using metadata

    This component loads the info dictionary once the document is ready and renders the common fields in a definition list:

    function DocumentInfo({ pdfDocument }) {
    const [info, setInfo] = useState(null);
    useEffect(() => {
    pdfDocument.getMetadata().then(({ info }) => setInfo(info));
    }, [pdfDocument]);
    if (!info) return null;
    return (
    <div>
    <h3>Document Properties</h3>
    <dl>
    <dt>Title</dt><dd>{info.Title || "Untitled"}</dd>
    <dt>Author</dt><dd>{info.Author || "Unknown"}</dd>
    <dt>Creator</dt><dd>{info.Creator}</dd>
    <dt>Pages</dt><dd>{pdfDocument.numPages}</dd>
    <dt>PDF Version</dt><dd>{info.PDFFormatVersion}</dd>
    <dt>Has Forms</dt><dd>{info.IsAcroFormPresent ? "Yes" : "No"}</dd>
    </dl>
    </div>
    );
    }

    File attachments

    Some PDFs contain embedded files (spreadsheets, images, other PDFs):

    const attachments = await pdfDocument.getAttachments();

    Attachments structure

    Attachments come back as an object keyed by file name, with each entry holding the raw bytes and an optional description:

    {
    "report.xlsx": {
    filename: "report.xlsx",
    content: Uint8Array(...), // Raw file data.
    description: "Quarterly report data",
    },
    "photo.jpg": {
    filename: "photo.jpg",
    content: Uint8Array(...),
    },
    }

    Downloading attachments

    This component lists each attachment and wraps its bytes in a Blob so the browser can download it when a reader clicks it:

    function AttachmentList({ pdfDocument }) {
    const [attachments, setAttachments] = useState(null);
    useEffect(() => {
    pdfDocument.getAttachments().then(setAttachments);
    }, [pdfDocument]);
    if (!attachments) return <div>No attachments</div>;
    const downloadAttachment = (attachment) => {
    const blob = new Blob([attachment.content]);
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = attachment.filename;
    a.click();
    URL.revokeObjectURL(url);
    };
    return (
    <ul>
    {Object.entries(attachments).map(([name, attachment]) => (
    <li key={name}>
    <button onClick={() => downloadAttachment(attachment)}>
    {attachment.filename}
    </button>
    {attachment.description && <span> — {attachment.description}</span>}
    </li>
    ))}
    </ul>
    );
    }

    Optional content groups (layers)

    PDFs can contain toggleable layers, which are common in computer-aided design (CAD) drawings, maps, and technical documents:

    const optionalContentConfig = await pdfDocument.getOptionalContentConfig();
    // List all groups.
    const groups = optionalContentConfig.getGroups();
    // { "layer-id-1": { name: "Dimensions", visible: true }, ... }
    // Toggle a layer.
    optionalContentConfig.setVisibility("layer-id-1", false);
    // Rerender affected pages after toggling.
    viewer.optionalContentConfigPromise = Promise.resolve(optionalContentConfig);

    Key points

    • getOutline() returns the full bookmark tree — render it as a sidebar for navigation
    • getMetadata() returns both an info dict and XMP metadata
    • getAttachments() returns embedded files as Uint8Array content — ready to download
    • Use linkService.goToDestination() to navigate to outline destinations
    • Optional content groups let you toggle visibility of PDF layers
    • All these APIs are on PDFDocumentProxy — call them after the document loads
    • These features are entirely built into PDF.js — no extra dependencies needed

    How Nutrient Web SDK handles this

    Nutrient Web SDK ships an interactive sidebar with bookmark, thumbnail, and annotation views — toggling between them is one setViewState call. Metadata is exposed through a typed getDocumentInfo() method, so you don’t need to walk a free-form info dictionary:

    instance.setViewState((v) =>
    v.set("sidebarMode", NutrientViewer.SidebarMode.BOOKMARKS),
    );
    const documentInfo = await instance.getDocumentInfo();
    console.log(documentInfo.title, documentInfo.author);

    That replaces the custom outline tree, sidebar UI, and linkService.goToDestination() wiring with two API calls. For navigation between pages and bookmark targets, see the related guides on thumbnail sidebars and navigation, zoom, and rotation.

    Learn more about Nutrient Web SDK | Migration guide | Contact Sales

    FAQ

    What’s the difference between an outline and a bookmark?

    In PDF terminology they’re the same thing — the PDF spec calls it an “outline,” but every PDF reader UI labels it “bookmarks.” pdfDocument.getOutline() returns the same tree that Acrobat shows in its bookmarks panel.

    Why does getOutline() return null sometimes?

    Because not every PDF has bookmarks. getOutline() returns null (not an empty array) when the document has no outline dictionary. Branch on outline === null in your UI to show a “no outline available” state rather than rendering an empty list.

    What’s in metadata.metadata vs. metadata.info?

    info is the legacy Document Information Dictionary — flat key-value pairs like Title, Author, Producer. metadata is an XMP packet, an Extensible Markup Language (XML) document that can carry arbitrary structured metadata (rights, history, custom schemas). Read XMP values with metadata.get("dc:title") or metadata.getAll(). Many PDFs have both; modern producers prefer XMP.

    Can I read metadata from a password-protected PDF?

    Only after the document loads, which requires the password. PDF.js’s getDocument({ url, password }) decrypts the file before exposing any API; once loadingTask.promise resolves, all the methods on this page work normally.

    Do typical PDFs have optional content groups (layers)?

    Rarely. Layers show up in CAD exports, geographic information system (GIS) maps, multi-language documents, and some technical drawings. For a typical business PDF — invoices, reports, contracts — getOptionalContentConfig().getGroups() returns an empty object. It’s worth handling, but don’t expect to find layers in most files.

    What format are file attachments in?

    Whatever format the original file was. getAttachments() returns a Uint8Array of raw bytes per attachment — the original file could be .xlsx, .docx, .jpg, another .pdf, or anything else. The filename field gives you the original name with its extension, and the browser’s Blob API can wrap the bytes for download or further processing.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?