# Lector: complete documentation These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. # Build a PDF viewer with Lector Start with a readable document, then add the controls your app needs. Source: https://anara.com/lector/docs These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Lector is a headless PDF viewer for React. PDF.js loads and renders the document; Lector provides page virtualization, composable layers, and hooks for navigation, search, and selection. You own the toolbar, layout, and application state. ## Start here 1. [Install Lector](https://anara.com/lector/docs/installation) and configure the PDF.js worker. 2. [Build your first viewer](https://anara.com/lector/docs/basic-usage) with selectable text, a toolbar, and a visible loading state. 3. [Load your own documents](https://anara.com/lector/docs/document-loading), including authenticated URLs and local files. Already integrating Lector? Keep the [API reference](https://anara.com/lector/docs/api) and [troubleshooting guide](https://anara.com/lector/docs/troubleshooting) nearby. ## Add a feature | I want to… | Guide | | ------------------------------------------- | --------------------------------------------------------------------- | | See a minimal viewer running | [Basic example](https://anara.com/lector/docs/code/basic) | | Go to a page or build previous/next buttons | [Page navigation](https://anara.com/lector/docs/code/page-navigation) | | Fit a document to its container | [Zoom controls](https://anara.com/lector/docs/code/zoom-control) | | Navigate using page previews | [Thumbnails](https://anara.com/lector/docs/code/thumbnails) | | Find text and jump to a match | [Search](https://anara.com/lector/docs/code/search) | | Draw a citation or a saved region | [Highlights](https://anara.com/lector/docs/code/highlight) | | Capture selected text and its coordinates | [Text selection](https://anara.com/lector/docs/code/select) | | Enable links inside a PDF | [PDF links](https://anara.com/lector/docs/code/links) | | Fill and download a PDF form | [PDF forms](https://anara.com/lector/docs/code/pdf-form) | | Render light text on dark paper | [Dark mode](https://anara.com/lector/docs/dark-mode) | ## What your application supplies Lector supplies primitives, not a finished reader. Give the viewer a bounded height, label its controls, and decide how to handle errors and save user data. Search reads a PDF's existing text; it does not perform OCR. Highlight overlays live in memory until your application stores them, and they do not modify the PDF file. These docs track the repository's `main` branch. If an API is missing from your installed package, compare its version with the [releases](https://github.com/anaralabs/lector/releases). The current source declares React 19+ and `pdfjs-dist` `^5.5.207` as peer dependencies. Contributing to Lector itself? See the [contributor guide](https://github.com/anaralabs/lector/blob/main/CONTRIBUTING.md). --- # AI agents and MCP Connect a coding assistant to Lector's guides, search the docs, or fetch complete Markdown without a browser. Source: https://anara.com/lector/docs/agents These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Give your assistant access to the same guides and examples you read here. Every page has a Markdown export, and the MCP server offers search and retrieval over the complete documentation catalog. No API key is required. These docs describe the source deployed with this site and track `main`. They are **not versioned by npm release**. Before applying an example, have your assistant inspect your installed `@anaralabs/lector`, `pdfjs-dist`, and React versions. Use the [release history](https://github.com/anaralabs/lector/releases) when working with an older package. ## Connect an MCP client Add a remote server in your assistant's MCP settings: | Setting | Value | | -------------- | ------------------------------------- | | Name | `lector` | | Server URL | `https://anara.com/lector/mcp` | | Transport | Streamable HTTP | | Authentication | None; public, read-only documentation | For clients that use an `mcpServers` configuration with a `url` field: ```json { "mcpServers": { "lector": { "url": "https://anara.com/lector/mcp" } } } ``` Configuration keys vary by client. If yours asks for a transport type, select HTTP or Streamable HTTP. This endpoint is not a local `stdio` command or the older HTTP+SSE transport with a separate `/sse` URL. The server also supports clients using the 2025 MCP initialization handshake. After connecting, ask the assistant to list the Lector documentation. It should discover these tools: | Tool | Input | Result | | ------------- | ------------------------- | --------------------------------------------------------------------- | | `list_docs` | `{}` | All page slugs, descriptions, URLs, resource URIs, and content hashes | | `search_docs` | `query`, optional `limit` | Ranked excerpts and links; defaults to 5 results, maximum 10 | | `get_doc` | `slug` | A complete Markdown guide, including its fenced code examples | Search accepts 1–200 characters. Use focused keywords such as `worker`, `Next.js`, `useSearch`, or `dark mode`. Results are keyword-ranked, not semantic search. An empty results list means no matching terms; try the API name or use `list_docs` to browse. Use the exact slug from a result: `installation`, `code/search`, or `index` for the overview. `get_doc` does not accept arbitrary URLs or local file paths. ### A useful first request ```text Use the Lector docs to add a PDF viewer to this app. Inspect the installed dependencies and framework first. Read installation and basic-usage, then the relevant feature guides. Include worker setup, selectable text, a defined viewer height, loading and error states, and controls that follow the app theme. Verify the implementation against the documented API. ``` For a specific task, search first and then read the complete result. Excerpts help find a guide; they may omit prerequisites or limitations that matter when writing code. Clients that support **resources** can list and read `lector://docs` for the JSON catalog and `lector://docs/` for individual Markdown pages. For example, `lector://docs/code/search` contains the search guide. Clients with **prompts** can run `build_pdf_viewer` with `framework: "react"` or `framework: "nextjs"` to start with the installation and first-viewer guides already included. The server only reads Lector documentation. It does not open your PDFs, access your repository, change files, or provide a PDF-processing service. Your assistant's other tools perform application work. ## Fetch docs without MCP An agent with HTTP access can read any of these directly: | URL | When to use it | | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | [/llms.txt](https://anara.com/lector/llms.txt) | Start here: concise project context and links to every guide | | [/llms.json](https://anara.com/lector/llms.json) | Discover page slugs, URLs, resource URIs, and SHA-256 content hashes programmatically | | [/docs/installation.md](https://anara.com/lector/docs/installation.md) | Read one complete page; append `.md` to a guide URL | | [/docs/index.md](https://anara.com/lector/docs/index.md) | Read the `/docs` overview as Markdown | | [/llms-full.txt](https://anara.com/lector/llms-full.txt) | Fetch every guide in one response for tools with enough context | ```bash curl -fsSL https://anara.com/lector/llms.txt curl -fsSL https://anara.com/lector/docs/code/search.md ``` Each HTML guide advertises its Markdown version through a `rel="alternate"` link. Markdown responses include a link to the discovery file. See the [llms.txt proposal](https://llmstxt.org/) for the discovery convention. Markdown is generated from the MDX used to build the site. Tables, links, and fenced source examples are retained. Executable MDX imports are removed, and interactive demos become a pointer to the browser page. A guide's `sha256` and the catalog's `revision` identify its exported content, so a client can detect changes without relying on a handwritten update date. ## Connection problems | Symptom | Check | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Opening `/mcp` in a browser returns 405 | Expected: use an MCP client that sends POST requests. For browser-readable content, use the docs or Markdown URLs. | | The client tries `/sse` or requires a command | Configure a remote Streamable HTTP server, or use direct Markdown fetching if the client only supports local servers. | | Tools work but resources or prompts are missing in the UI | Client support varies. All documentation is also available through the three tools. | | A browser-based client receives 403 | The deployment must explicitly allow that client's origin. Native clients without an `Origin` header can connect directly. | | Search finds too much or nothing | Use a distinctive API name, reduce the query to keywords, or browse `list_docs`. | | An example uses an export absent from your app | Compare installed versions with the source and releases; the corpus is not pinned to your npm version. | For a self-hosted site, replace the hostname above with your deployment's URL. The [maintainer guide](https://github.com/anaralabs/lector/blob/main/packages/docs/README.md#agent-documentation-and-mcp) covers origin configuration, validation, and how new pages enter the catalog. --- # API reference Component contracts, defaults, hook scope, and public exports in the current source. Source: https://anara.com/lector/docs/api These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Import public APIs from `@anaralabs/lector`. This reference follows `packages/lector/src/index.ts` and the implementation on `main`; compare your installed version with the [releases](https://github.com/anaralabs/lector/releases) if a symbol is missing. Do not import from `src/internal` or a package-internal path. ## Root Loads one document and supplies its context. Accepts HTML `div` props in addition to the document options below. Its `onError` is a Lector callback, not a DOM error handler. | Prop | Default | Contract | | ----------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `source` | Required | URL string, `URL`, typed array, `ArrayBuffer`, or PDF.js loading-parameters object | | `loader` | `"Loading..."` | Content shown until the document and viewports are ready | | `onDocumentLoad` | — | Receives `{ proxy, source }` after PDF.js loads the document, before viewport generation | | `onError` | — | Receives `{ error, phase, source }` for initialization failures | | `documentOptions` | Lector defaults | PDF.js loading-parameter overrides; applied on the next load | | `zoom` | `1` | Initial zoom multiplier | | `isZoomFitWidth` | `false` | Initial fit-width mode | | `zoomOptions` | `{ minZoom: 0.5, maxZoom: 10 }` | Initial limits for zoom updates | | `colorScheme` | `"light"` | `"light"` or `"dark"`; changes synchronize with the store | | `darkModeColors` | See [dark mode](https://anara.com/lector/docs/dark-mode) | Optional `background` and `foreground`; changes synchronize with the store | `source` changes trigger loading. Keep object and binary sources stable. The zoom props initialize state; use store actions for subsequent changes. Changing `documentOptions` alone does not reload the document. See [loading documents](https://anara.com/lector/docs/document-loading) for source handling, retries, and asset paths. The current type also contains `initialRotation`, but `Root` does not forward it into document initialization. Do not rely on it for rotation controls. ## Pages and Page | Component / prop | Default | Contract | | -------------------------- | ----------------- | --------------------------------------------------------- | | `Pages.children` | Required | One React element, normally a `Page` template | | `Pages.gap` | `10` | Gap between virtualized pages | | `Pages.virtualizerOptions` | `{ overscan: 1 }` | Extra pages mounted outside the visible range | | `Pages.initialOffset` | — | Initial vertical scroll offset in pixels | | `Pages.onOffsetChange` | — | Reports nonzero scroll offsets; zero is currently omitted | | `Page.pageNumber` | `1` | One-based page number; supplied automatically by `Pages` | Both accept HTML `div` props. `Pages` supplies its own scrolling and a default height of 100%; its parent needs a definite height. `Page` establishes page-number context and owns viewport dimensions. See [layout](https://anara.com/lector/docs/basic-usage#give-the-pages-room-to-scroll). ## Page layers All page layers belong under `Page`. | Component | Purpose and special props | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `CanvasLayer` | Visible content; accepts canvas props and optional `background` | | `TextLayer` | Selectable text; accepts div props and adds PDF.js's `textLayer` class | | `AnnotationLayer` | Existing PDF links and fields; `renderForms=true`, `externalLinksEnabled=true`, `jumpOptions={ behavior: "smooth", align: "start" }` | | `HighlightLayer` | Draws current `highlights`; div props and `asChild` apply to each rectangle | | `ColoredHighlightLayer` | Color-selection tools and stored colored overlays; `onHighlight(highlight)` reports a new record | | `CustomLayer` | Render prop: `children(pageNumber)` returns JSX; position your own overlay | | `AnnotationHighlightLayer` | Draws application annotation records; used with `AnnotationsStoreProvider` | `CanvasLayer` does not provide searchable DOM text. `HighlightLayer` needs a visible color or border from your styles. Links and fields need the PDF.js stylesheet. ## Controls and thumbnails Mount document controls under `Root`. | Export | Contract | | -------------------------- | ---------------------------------------------------------------------------------------------------------- | | `CurrentPage` | Labeled number input; commits on blur or Enter | | `TotalPages` | A div containing the page count | | `CurrentZoom` | Labeled input displaying zoom as a percentage | | `ZoomIn`, `ZoomOut` | Buttons that change zoom by 0.1; supply text, labels, and `type="button"` | | `Thumbnails` | Clones one thumbnail template per page | | `Thumbnail` | Canvas preview with a one-based `pageNumber`; click or Enter navigates | | `NextPage`, `PreviousPage` | Exported placeholders; use [custom navigation buttons](https://anara.com/lector/docs/code/page-navigation) | `Outline`, `OutlineItem`, and `OutlineChildItems` compose a document outline. Their current destination handling has page-index limitations, so verify navigation against your PDFs before using them. For a custom outline, the PDF.js document proxy exposes `getOutline()` and `getDestination()`. Convert PDF.js's zero-based `getPageIndex()` result to a one-based page number before calling `jumpToPage`. ## usePdf `usePdf(selector)` subscribes to the store belonging to the nearest `Root`. Select only the values your component needs. The provider is unavailable in `Root`'s loader and outside its children. ```tsx title="fit-width-button.tsx" "use client"; import { usePdf } from "@anaralabs/lector"; export default function FitWidthButton() { const fitWidth = usePdf((state) => state.zoomFitWidth); return ; } ``` These are the store fields most useful to application code: | Read | Update / action | | --------------------------------------- | ------------------------------------------------------------------------------------- | | `pdfDocumentProxy` | PDF.js document methods such as `saveDocument()` | | `currentPage` | Use `usePdfJump().jumpToPage()` to navigate; `setCurrentPage()` alone does not scroll | | `zoom`, `isZoomFitWidth`, `zoomOptions` | `updateZoom(numberOrUpdater, isZoomFitWidth?)`, `zoomFitWidth()` | | `colorScheme`, `darkModeColors` | `setColorScheme(scheme, colors?)` | | `highlights` | `setHighlight(rectangles)` replaces the array | | `coloredHighlights` | `addColoredHighlight(record)`, `deleteColoredHighlight(uuid)` | | `viewports` | Scale-1 PDF.js page viewports, indexed from 0 | | `pageProxies` | `getPdfPageProxy(pageNumber)` takes a one-based page number | | `textContent` | Populated by `Search` | The store also exposes rendering and virtualizer internals. Prefer the components and hooks above over mutating those fields. ## usePdfJump Requires `Root` and a mounted `Pages` virtualizer. | Method | Contract | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `jumpToPage(page, options?)` | One-based page; `align` defaults to `"start"`, `behavior` to `"smooth"` | | `jumpToOffset(offset)` | Scroll pixels; smooth scrolling | | `jumpToHighlightRects(rects, type, align?, additionalOffset?)` | Replaces highlights and scrolls; `type` is `"pixels"` or `"percent"` | | `scrollToHighlightRects(rects, type, align?, additionalOffset?, behavior?)` | Scrolls without replacing highlights; returns whether an offset could be resolved | For rectangle navigation, `align` is `"start"` or `"center"` (default `"start"`), `additionalOffset` defaults to `0`, and scroll behavior defaults to `"smooth"`. Use [the coordinate contract](https://anara.com/lector/docs/code/highlight#coordinate-contract) when supplying your own rectangles. ## Search and selection | Export | Scope and result | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Search` | Under `Root`; indexes every page's text; accepts `children`, `loading`, and `errorFallback({ error, retry })` | | `useSearch()` | Under `Root`; returns `search`, `searchAsync`, `cancelSearch`, `isSearching`, `searchResults`, `textContent`, and `keywords` (currently an empty array) | | `calculateHighlightRects(pageProxy, textPosition)` | Async utility returning `HighlightRect[]` for a text match | | `SelectionTooltip` | Under `Root`; displays children for a selection; mount once per viewer | | `useSelectionDimensions()` | Under `Root`; `getDimension()` reads text and rectangles, `getAnnotationDimension()` also computes underlines | | `usePDFPageNumber()` | Under `Page`; current one-based page number | | `usePageRendered(pageNumber)` | Under `Root`; reads whether that page is marked rendered | Use `getDimension()` with an undefined check. The `getSelection()` alias has a stronger return type than its runtime guarantee, so it does not remove that need. See [search](https://anara.com/lector/docs/code/search) for options, result allocation, and indexing cost, and [selection](https://anara.com/lector/docs/code/select) for a complete action. ## Annotation and link integrations The package also exports `AnnotationsStoreProvider`, `useAnnotations`, `AnnotationTooltip`, and the `Annotation` and `AnnotationTooltipContentProps` types for application-owned annotations. These use a separate annotation store; `Root` does not install that provider for you. Wrap each independent annotation session in `AnnotationsStoreProvider`; without it, `useAnnotations` uses a shared fallback store. The docs site's [annotation demo source](https://github.com/anaralabs/lector/tree/main/packages/docs/app/\(home\)/_components) shows their composition. `LinkService`, `PDFLinkServiceContext`, `useCreatePDFLinkService`, and `usePDFLinkService` expose the PDF link integration. `Root` installs the service; `AnnotationLayer` connects destination changes to scrolling. Use [PDF links](https://anara.com/lector/docs/code/links) for the normal integration. ## Public types and color utilities `HighlightRect`, `ColoredHighlight`, `SearchResult`, and `SearchResults` describe overlay and search data. `ColorScheme`, `DarkModeColors`, and `RenderColorMap` describe color configuration. `DEFAULT_DARK_MODE_COLORS` and `createDarkModeColorMap` let your overlays use the same palette as the page. See [dark mode](https://anara.com/lector/docs/dark-mode). Component props are inferred from the components rather than all being exported as named interfaces. For a wrapper, derive them with `React.ComponentProps` (or the component you wrap). --- # Your first viewer A complete viewer with selectable text, page and zoom controls, and explicit layout. Source: https://anara.com/lector/docs/basic-usage These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Complete the [installation](https://anara.com/lector/docs/installation) first. This example imports the `pdf-setup.ts` you created there and loads `public/sample.pdf`. In Next.js, render it through the browser-only wrapper from that guide. ## A viewer you can build on ```tsx title="pdf-viewer.tsx" "use client"; import { CanvasLayer, CurrentPage, CurrentZoom, Page, Pages, Root, TextLayer, TotalPages, ZoomIn, ZoomOut, } from "@anaralabs/lector"; import "./pdf-setup"; export default function PDFViewer() { return ( Loading PDF…

} >
of +
); } ``` `CurrentPage` and `CurrentZoom` are editable inputs, so they need labels. The zoom buttons have no default text; supply their children. `CurrentPage` commits when the input loses focus or you press Enter. ## How the pieces fit ```text Root Loads one document and provides its store ├── Your toolbar Reads and changes that document's state └── Pages Owns the scroll container and virtualizes pages └── Page A template cloned for each visible page ├── CanvasLayer Paints the page └── TextLayer Adds selectable text over the canvas ``` Pass **one `Page` template** to `Pages`. It supplies the page number; you do not map over the document yourself. Page numbers in Lector's public navigation APIs start at **1**. `Root` only mounts its children after the document and page viewports are ready. Components using `usePdf`, `usePdfJump`, or other viewer hooks must be descendants of `Root`. Page-specific layers and `usePDFPageNumber` also need a `Page` ancestor. A hook in the same component that *returns* `Root` is still outside that provider. ## Give the pages room to scroll `Pages` defaults to `height: 100%` and owns its scrolling. Give its parent a definite height. With a toolbar, use a flex column and put `Pages` in a `flex: 1; min-height: 0` wrapper, as above. In a sidebar layout, also set `min-width: 0` on the viewer column. Avoid overriding `Page` dimensions or applying your own scale transform: Lector uses its viewports to align canvas, text, and highlights. ## Choose your layers | Layer | Adds | Guide | | ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | | `CanvasLayer` | Visible PDF content | [Basic example](https://anara.com/lector/docs/code/basic) | | `TextLayer` | Selection and copying when the PDF contains text | [Text selection](https://anara.com/lector/docs/code/select) | | `AnnotationLayer` | Existing PDF links and form widgets | [Links](https://anara.com/lector/docs/code/links), [forms](https://anara.com/lector/docs/code/pdf-form) | | `HighlightLayer` | Rectangles from the viewer's `highlights` state | [Highlights](https://anara.com/lector/docs/code/highlight) | | `ColoredHighlightLayer` | Selection color tools and colored highlights | [Text selection](https://anara.com/lector/docs/code/select#saving-highlights) | Place canvas first, then text, then the interaction or highlight layers you need. Loading failures need application UI outside `Root`; see [loading and errors](https://anara.com/lector/docs/document-loading#show-errors-and-retry). Next, add [page navigation](https://anara.com/lector/docs/code/page-navigation), [search](https://anara.com/lector/docs/code/search), or [dark mode](https://anara.com/lector/docs/dark-mode). --- # Basic example The smallest viewer, with visible pages and selectable text. Source: https://anara.com/lector/docs/code/basic These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. This viewer renders a PDF with a canvas and a text layer. Complete [installation](https://anara.com/lector/docs/installation), create `pdf-setup.ts`, and put a PDF at `public/sample.pdf` before using the code. ```tsx title="basic-viewer.tsx" "use client"; import { CanvasLayer, Page, Pages, Root, TextLayer } from "@anaralabs/lector"; import "./pdf-setup"; export default function BasicViewer() { return ( Loading PDF…

} >
); } ``` The fixed height gives `Pages` a scrollable viewport. Remove `TextLayer` if you only need the page image. To follow your app theme, pass `colorScheme="dark"` or `"light"` to `Root`; the live example below follows the docs theme. Continue with [a toolbar](https://anara.com/lector/docs/basic-usage), [error handling](https://anara.com/lector/docs/document-loading#show-errors-and-retry), or [links and form widgets](https://anara.com/lector/docs/code/links). ## Live example Open the documentation page linked above to use this interactive example. --- # Highlights Draw and navigate to rectangles in page coordinates. Source: https://anara.com/lector/docs/code/highlight These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. `HighlightLayer` draws the rectangles in `usePdf((state) => state.highlights)`. Use it for search results, citation targets, or a selected region. It does not extract text, save data, or write annotations into the PDF. ## Show a region and scroll to it Complete [installation](https://anara.com/lector/docs/installation) first. The rectangle below is an illustrative region on page 1; replace it with coordinates from your document. ```tsx title="highlight-viewer.tsx" "use client"; import { CanvasLayer, HighlightLayer, Page, Pages, Root, TextLayer, usePdf, usePdfJump, type HighlightRect, } from "@anaralabs/lector"; import "./pdf-setup"; const region: HighlightRect[] = [ { pageNumber: 1, left: 10, top: 15, width: 70, height: 5, type: "percent" }, ]; function HighlightControls() { const { jumpToHighlightRects } = usePdfJump(); const setHighlight = usePdf((state) => state.setHighlight); return (
); } export default function HighlightViewer() { return (
); } ``` To draw without scrolling, call `setHighlight(rects)`. It **replaces** the active rectangles. To scroll without replacing them, use `scrollToHighlightRects` from `usePdfJump`. ## Coordinate contract | Field | Meaning | | ----------------- | ------------------------------------------------------------ | | `pageNumber` | One-based page number | | `left`, `top` | Distance from the page's top-left corner | | `width`, `height` | Rectangle size in the same units | | `type` | `"pixels"` (also used when omitted) or `"percent"` | | `style` | Optional function from the rectangle to React CSS properties | Pixel coordinates use the page's scale-1 viewport, not screen pixels or raw PDF bottom-left coordinates. Selection and search helpers return this format. For percentage coordinates, `10` means 10%, not `0.1`. Set `type: "percent"` on **each rectangle** and also pass `"percent"` to the jump helper. The rectangle controls drawing; the helper argument controls scrolling. Mixing the units makes the viewport and overlay disagree. For coordinates from a backend PDF parser, account for the PDF crop box, rotation, and origin before drawing. You can obtain the page through `getPdfPageProxy(pageNumber)` and use PDF.js viewport conversion methods. ## Styling and persistence The layer supplies positioning and `pointer-events: none`; give it a background color or border so it is visible. Props and styles apply to every rectangle. Avoid opaque fills that hide the text. Save serializable rectangle data with a document ID and revision if it must survive a reload. A `style` callback is not serializable; reconstruct styling in your app. Changing the source or unmounting `Root` creates a new viewer state. See [selection](https://anara.com/lector/docs/code/select) for capturing text and coordinates and [dark mode](https://anara.com/lector/docs/dark-mode) for overlay colors. ## Live example Open the documentation page linked above to use this interactive example. --- # PDF links Enable existing PDF links and choose external-link and navigation behavior. Source: https://anara.com/lector/docs/code/links These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. `AnnotationLayer` renders annotations already present in the PDF, including links and form widgets. It needs the PDF.js stylesheet from [installation](https://anara.com/lector/docs/installation). It does not turn arbitrary text URLs into links. ## Enable links ```tsx title="linked-viewer.tsx" "use client"; import { AnnotationLayer, CanvasLayer, Page, Pages, Root, TextLayer } from "@anaralabs/lector"; import "./pdf-setup"; export default function LinkedViewer() { return ( ); } ``` ## Options | Prop | Default | Behavior | | ---------------------- | ---------- | ------------------------------------------------ | | `externalLinksEnabled` | `true` | Enables links to external URLs | | `renderForms` | `true` | Renders interactive form fields as well as links | | `jumpOptions.behavior` | `"smooth"` | `"smooth"` or `"auto"` for internal navigation | | `jumpOptions.align` | `"start"` | `"start"`, `"center"`, or `"end"` | Internal links navigate within the current document. When a destination includes a position, Lector can scroll to that position within the page. External links use the link service and open in a new tab by default. Set `externalLinksEnabled={false}` when your viewer should disable them. Keep these settings consistent across page layers in a viewer: they share one link service. ## Custom navigation For ordinary page buttons, use [usePdfJump](https://anara.com/lector/docs/code/page-navigation). For PDF-specific destinations, `usePDFLinkService()` exposes the underlying service, including `goToDestination(name)` and `page`. Destination navigation depends on the viewer scroll integration established by `AnnotationLayer`; a bare service without that integration is not a substitute for `usePdfJump`. ## Styling links You can target the PDF.js annotation classes in your application's stylesheet: ```css .annotationLayer .linkAnnotation > a:hover { background: rgb(255 220 80 / 20%); } .annotationLayer .linkAnnotation > a:focus-visible { outline: 2px solid currentColor; } ``` If a link does nothing, first confirm the file contains an actual PDF link annotation. Then check that `AnnotationLayer` is mounted, external links are enabled if needed, and an overlay is not intercepting pointer events. See [PDF forms](https://anara.com/lector/docs/code/pdf-form) to save filled form fields. ## Live example Open the documentation page linked above to use this interactive example. --- # Page navigation Build labeled previous and next buttons and jump to a specific page. Source: https://anara.com/lector/docs/code/page-navigation These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Use `usePdfJump` to move the scroll viewport. Lector's page numbers start at **1**, including `currentPage` and `jumpToPage(1)`. ## Previous, next, and direct input This toolbar is a complete component. Mount `` **inside `Root`**, above the bounded `Pages` wrapper from [your first viewer](https://anara.com/lector/docs/basic-usage). ```tsx title="page-controls.tsx" "use client"; import { CurrentPage, TotalPages, usePdf, usePdfJump } from "@anaralabs/lector"; export default function PageControls() { const page = usePdf((state) => state.currentPage); const total = usePdf((state) => state.pdfDocumentProxy.numPages); const { jumpToPage } = usePdfJump(); return (
of
); } ``` `CurrentPage` updates as you scroll. Its input commits on blur or Enter. For your own input, validate an integer from `1` through `numPages` before calling `jumpToPage`; that hook does not validate the range for you. The exported `NextPage` and `PreviousPage` components are currently placeholders. Use the hook-based buttons above. ## Jump options ```ts jumpToPage(3, { align: "start", behavior: "auto" }); ``` | Option | Values | Default | | ---------- | ---------------------------------------- | ---------- | | `align` | `"start"`, `"center"`, `"end"`, `"auto"` | `"start"` | | `behavior` | `"auto"`, `"smooth"` | `"smooth"` | Use `"auto"` for immediate jumps and when the user prefers reduced motion. `jumpToPage` needs a mounted `Pages` container; before its virtualizer exists, the call does nothing. Changing `setCurrentPage` alone changes store state but does not scroll. ## Restore a reading position `Pages` accepts `initialOffset` in scroll pixels and reports changes through `onOffsetChange`. Save the offset under a document-specific key, then pass it when mounting that document again. The current callback only reports nonzero offsets, so reset a saved position explicitly when your app returns to the top. Offsets depend on zoom, page sizes, and layout. For bookmarks that need to survive layout changes, save a page number or a [highlight region](https://anara.com/lector/docs/code/highlight) instead. ## Live example Open the documentation page linked above to use this interactive example. --- # PDF forms Render interactive fields and download a PDF containing the filled values. Source: https://anara.com/lector/docs/code/pdf-form These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Add `AnnotationLayer` with `renderForms` to render the PDF's interactive fields. Form state belongs to PDF.js's document annotation storage. Use the document proxy's `saveDocument()` method when you need PDF bytes with the edits applied. ## Fill and download Use a PDF with actual interactive form fields, such as `public/form.pdf`, and the `pdf-setup.ts` from [installation](https://anara.com/lector/docs/installation). ```tsx title="form-viewer.tsx" "use client"; import { AnnotationLayer, CanvasLayer, Page, Pages, Root, TextLayer, usePdf } from "@anaralabs/lector"; import { useState } from "react"; import "./pdf-setup"; function DownloadFilledPDF() { const document = usePdf((state) => state.pdfDocumentProxy); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); async function download() { setSaving(true); setError(null); try { const bytes = await document.saveDocument(); const blob = new Blob([new Uint8Array(bytes)], { type: "application/pdf" }); const url = URL.createObjectURL(blob); const link = window.document.createElement("a"); link.href = url; link.download = "filled-form.pdf"; window.document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); } catch { setError("Couldn't save this PDF. Please try again."); } finally { setSaving(false); } } return (
{error &&

{error}

}
); } export default function FormViewer() { return (
); } ``` `saveDocument()` returns a promise for the edited PDF bytes. Downloading the source URL or calling `getData()` is not the same operation. See the [PDF.js document proxy reference](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFDocumentProxy.html#saveDocument). ## Reading values in your application Do not use `new FormData(viewerElement)` as a complete document export. `Pages` virtualizes the document, so offscreen widgets may not exist in the DOM. `FormData` also follows HTML successful-control rules; unchecked or disabled controls can be absent. The proxy exposes `annotationStorage` for PDF.js annotation state and `getFieldObjects()` for field metadata. If you need a business-domain JSON object, map PDF field identifiers to your schema explicitly and validate the values in your app. Storage is PDF.js state, not a ready-made object keyed by your application's field names. The live example below inspects currently mounted DOM form controls for demonstration. Use the download recipe above for document-wide PDF output. ## Boundaries This supports interactive PDF form widgets, not arbitrary PDF text editing. A printed blank line in a scanned document is not a field. Test the specific form types your app accepts and verify the downloaded file in another PDF reader. Lector's custom highlight and comment overlays are separate from PDF.js form storage. `saveDocument()` does not automatically embed those overlays in the saved PDF. ## Live example Open the documentation page linked above to use this interactive example. --- # Search Index document text, display matches, and jump to highlighted results. Source: https://anara.com/lector/docs/code/search These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Search has three parts: `Search` extracts the document's text, `useSearch` finds matches, and `HighlightLayer` draws a selected result. Mount `Search` once per viewer. Keep `Pages` outside it so indexing does not replace the document with a loading message. ## A searchable viewer This example searches on submit to avoid doing fuzzy matching on every keystroke. It uses the `pdf-setup.ts` from [installation](https://anara.com/lector/docs/installation). ```tsx title="searchable-viewer.tsx" "use client"; import { CanvasLayer, HighlightLayer, Page, Pages, Root, Search, TextLayer, calculateHighlightRects, usePdf, usePdfJump, useSearch, type SearchResult, } from "@anaralabs/lector"; import { useState } from "react"; import "./pdf-setup"; function SearchPanel() { const [query, setQuery] = useState(""); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); const { search, searchResults } = useSearch(); const getPage = usePdf((state) => state.getPdfPageProxy); const { jumpToHighlightRects } = usePdfJump(); const setHighlight = usePdf((state) => state.setHighlight); const results = [...searchResults.exactMatches, ...searchResults.fuzzyMatches]; async function openResult(result: SearchResult) { setError(null); try { const rects = await calculateHighlightRects(getPage(result.pageNumber), result); jumpToHighlightRects(rects, "pixels", "center"); } catch { setError("Couldn't locate this match on the page."); } } return (
{ event.preventDefault(); search(query, { limit: 20 }); setHighlight([]); setSubmitted(true); setError(null); }}>

{submitted ? `${results.length} results shown` : "Enter text to search."}

{error &&

{error}

}
    {results.map((result) => (
  • ))}
); } export default function SearchableViewer() { return (
Indexing document…

}>
); } ``` `calculateHighlightRects` uses the result's page number, match index, and `searchText` to locate the match. Passing the result itself keeps highlighting tied to the submitted query even if the user has since edited the input. It returns pixel rectangles in page coordinates. ## Search options and results `search(text, options)` is synchronous. It returns the results and also updates `searchResults` in that hook instance. | Option | Default | Meaning | | ----------- | ------- | ------------------------------------------------------- | | `threshold` | `0.7` | Minimum fuzzy similarity; use a value from 0 to 1 | | `limit` | `10` | Result budget shared between exact and fuzzy groups | | `textSize` | `100` | Characters of trailing context to include after a match | Results contain `exactMatches`, `fuzzyMatches`, and `hasMoreResults`. Each match has `pageNumber`, `text`, `matchIndex`, `score`, `isExactMatch`, and optional `searchText`. Matching is case-insensitive. Searching an empty or whitespace-only query clears results. The current implementation allocates at most `ceil(limit / 2)` results to exact matches and the remaining budget to fuzzy matches. It can therefore return fewer than `limit` results even when more exact matches exist. `hasMoreResults` compares the total candidate count with `limit`; it is not a reliable count of everything omitted by the group allocation. If you build “show more,” rerun the submitted query with a higher limit rather than treating the result as paginated data. ## Practical limits `Search` extracts text from all pages, even though `Pages` only renders a viewport's worth of pages. Mount the search panel on demand for large PDFs. For search-as-you-type, debounce the query and measure with representative documents; lowering the result limit does not stop the full-document matching pass. Image-only scans need an OCR text layer before they can be searched. PDF text extraction can also differ from visual reading order or spacing. Check the extracted text when a phrase looks present but is not found. ## Live example Open the documentation page linked above to use this interactive example. ## Responsive search in long documents `useSearch()` also exposes `searchAsync`, `cancelSearch`, and `isSearching`. `searchAsync` returns the same ranked results as `search`, but yields between batches so the browser can process input. It defaults to an approximate 8 ms work budget. Scheduling can increase total search time; the benefit is responsiveness. ```tsx const { searchAsync, cancelSearch, searchResults, isSearching } = useSearch(); const [searchError, setSearchError] = useState(null); useEffect(() => { setSearchError(null); void searchAsync(debouncedSearchText, { limit: 10 }).catch((error) => { if (error.name !== "AbortError") setSearchError(error); }); return cancelSearch; }, [debouncedSearchText, searchAsync, cancelSearch]); ``` A newer call to either search method cancels pending async work from that hook. Unmounting or replacing document text also cancels it. Cancelled calls reject with `AbortError` and never publish partial results. You can pass an external `AbortSignal` through `{ signal }` or adjust `{ timeSliceMs }` when needed. Keep the component inside `` and use `` to index the document. ## Indexing failures and retry If text extraction fails, `Search` keeps its children unmounted and shows an error with a Retry button. Retry starts a new indexing attempt without reloading the viewer. To match your application's error presentation, provide a fallback: ```tsx (

Search is unavailable.

)} >
``` This fragment replaces the existing `Search` wrapper in the example above. The fallback also receives the original `error`. It handles indexing failures; `Root.onError` handles document loading failures. --- # Text selection Capture selected text and page-relative rectangles with a selection action. Source: https://anara.com/lector/docs/code/select These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Add `TextLayer` to make a text-based PDF selectable. `useSelectionDimensions().getDimension()` reads the current browser selection and returns the selected text and its highlight rectangles. It can return `undefined`, so check the result before using it. ## Turn a selection into a highlight This example uses `pdf-setup.ts` from [installation](https://anara.com/lector/docs/installation). Select text, then activate the Highlight button in the tooltip. ```tsx title="selection-viewer.tsx" "use client"; import { CanvasLayer, HighlightLayer, Page, Pages, Root, SelectionTooltip, TextLayer, usePdf, useSelectionDimensions, } from "@anaralabs/lector"; import "./pdf-setup"; function SelectionAction() { const { getDimension } = useSelectionDimensions(); const setHighlight = usePdf((state) => state.setHighlight); return ( ); } export default function SelectionViewer() { return ( ); } ``` Mount one selection tooltip per viewer, outside the repeated `Page` template. Preventing the button's mouse-down default keeps the browser from discarding the selection before the click handler reads it. The result contains `text`, `highlights`, and `isCollapsed`. Rectangles use [scale-1 page pixels](https://anara.com/lector/docs/code/highlight#coordinate-contract); a multi-page selection can contain multiple page numbers. `setHighlight` replaces the active highlight. The example does not accumulate selections or persist them. ## Saving highlights For a built-in color picker, add `ColoredHighlightLayer` under `Page`. Its `onHighlight` callback receives a `ColoredHighlight` with `uuid`, `text`, `color`, `pageNumber`, and `rectangles`. Its tools use Tailwind utility classes; see [styles](https://anara.com/lector/docs/installation#styles-and-pdf-assets). Your application owns storage. Save each highlight under a document ID and revision, report save failures, and restore saved records using the store's `addColoredHighlight`. Restore once per document session to avoid duplicate records. `deleteColoredHighlight(uuid)` removes one from the current store; delete it from your persistence layer separately. These are viewer overlays. They do not become embedded PDF annotations when downloading the original file. ## Selection limits A scanned page without embedded text cannot provide text selection; Lector does not run OCR. Selection geometry also depends on the PDF's text layout and which virtualized text layers are mounted. Test multi-column text, rotated pages, zoom, and touch selection with the PDFs your users work with. If text is selectable but displaced, check the PDF.js stylesheet and avoid adding your own transforms to `Page` or `TextLayer`. ## Live example Open the documentation page linked above to use this interactive example. --- # Thumbnails Add a scrollable page-preview sidebar beside the document. Source: https://anara.com/lector/docs/code/thumbnails These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. `Thumbnails` clones one `Thumbnail` template for every page. Each thumbnail navigates to its page on click or Enter. Both thumbnails and pages must share the same `Root`. ## A sidebar layout Complete [installation](https://anara.com/lector/docs/installation) and create `pdf-setup.ts` before using this example. ```tsx title="thumbnail-viewer.tsx" "use client"; import { CanvasLayer, Page, Pages, Root, TextLayer, Thumbnail, Thumbnails, } from "@anaralabs/lector"; import "./pdf-setup"; export default function ThumbnailViewer() { return (
); } ``` The sidebar scrolls independently. `minWidth: 0` allows the main viewer to shrink when the sidebar takes space. Set a CSS width on `Thumbnail`; Lector owns the canvas's drawing dimensions. ## Label individual pages For specific labels and an active-page indicator, map thumbnails yourself inside a child of `Root`: ```tsx title="labeled-thumbnails.tsx" "use client"; import { Thumbnail, usePdf } from "@anaralabs/lector"; export default function LabeledThumbnails() { const count = usePdf((state) => state.pdfDocumentProxy.numPages); const current = usePdf((state) => state.currentPage); return (
{Array.from({ length: count }, (_, index) => { const page = index + 1; return (
Page {page}
); })}
); } ``` Keep a visible focus indicator in your app's styles. The built-in keyboard handler supports Enter; if your UI needs Space activation too, provide an `onKeyDown` handler that prevents the page scroll and activates the thumbnail. `Thumbnails` creates a wrapper for every page; it is not the same virtualized list as `Pages`. Thumbnail canvas rendering is visibility-driven. For very large documents, measure the sidebar separately and consider mounting it only when opened. ## Live example Open the documentation page linked above to use this interactive example. ## Virtual thumbnails for long documents For hundreds or thousands of pages, opt into a bounded list. Give `Thumbnails` a bounded height and specify the fixed height of each row, including spacing: ```tsx ``` The container owns scrolling and mounts only nearby rows. Ensure each child's content fits `itemHeight`; margins, padding and spacing belong inside that row. The default layout remains available by omitting `virtualize`. With a thumbnail focused, Arrow Up/Down moves between pages, and Home/End moves to the first/last page, mounting the target before focusing it. Enter opens the focused page. Consumer keyboard handlers can call `preventDefault()` to override the virtual list's navigation keys. --- # Zoom controls Set zoom limits, build a toolbar, and keep pages fitted to the container. Source: https://anara.com/lector/docs/code/zoom-control These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Zoom values are multipliers: `1` means 100%, `0.5` means 50%. The `CurrentZoom` input displays a percentage. ## Add a toolbar Mount this component inside `Root`. It includes a fit-width action and disables zoom buttons at the configured limits. ```tsx title="zoom-controls.tsx" "use client"; import { CurrentZoom, ZoomIn, ZoomOut, usePdf } from "@anaralabs/lector"; export default function ZoomControls() { const zoom = usePdf((state) => state.zoom); const min = usePdf((state) => state.zoomOptions.minZoom); const max = usePdf((state) => state.zoomOptions.maxZoom); const fitWidth = usePdf((state) => state.zoomFitWidth); return (
Zoom out = max}>Zoom in
); } ``` ## Initial zoom and limits ```tsx {/* Your toolbar and bounded Pages container */} ``` The defaults are `zoom: 1`, `minZoom: 0.5`, and `maxZoom: 10`. Supply a positive minimum and a maximum at least as large as the minimum. Initial zoom and limits initialize the document store; they are not controlled props that update the zoom after mounting. For runtime changes, select `updateZoom` from `usePdf` and call `updateZoom(1.5)` or `updateZoom((previous) => previous + 0.1)`. Updates are clamped to the configured limits. `ZoomIn` and `ZoomOut` change zoom in steps of `0.1`. ## Fit width Set `isZoomFitWidth` on `Root` to start fitted to the container. `zoomFitWidth()` enables the same mode from a control. Fit mode responds to container resizing and respects zoom limits. A manual `updateZoom` call leaves fit mode by default. For a toolbar layout, keep `Pages` in a `flex: 1; min-height: 0` wrapper. See [your first viewer](https://anara.com/lector/docs/basic-usage) for the complete structure. Avoid CSS `transform: scale(...)` on the viewer: the library must know the zoom to position selection and overlays correctly. ## Live example Open the documentation page linked above to use this interactive example. --- # Dark mode Render dark PDF pages, synchronize your theme, and style overlays to match. Source: https://anara.com/lector/docs/dark-mode These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. `Root` accepts `colorScheme="dark"` to remap page colors during canvas rendering. It changes the document's pixels; your application still owns the toolbar, sidebar, and surrounding UI theme. ## Follow your app theme Use the `pdf-setup.ts` from [installation](https://anara.com/lector/docs/installation). Pass your resolved theme as a prop: ```tsx title="themed-viewer.tsx" "use client"; import { CanvasLayer, Page, Pages, Root, TextLayer } from "@anaralabs/lector"; import "./pdf-setup"; export default function ThemedViewer({ dark }: { dark: boolean }) { return ( ); } ``` Unlike initial zoom options, `colorScheme` updates the store when the prop changes. The canvas, thumbnails, and high-zoom detail layer use the active scheme. Cached bitmaps can be reused when available; switching is not guaranteed to be instantaneous for every document or cache state. ## Let a viewer control its own theme If you do not pass a `colorScheme` prop, a child of `Root` can switch the scheme through the store: ```tsx title="dark-mode-toggle.tsx" "use client"; import { usePdf } from "@anaralabs/lector"; export default function DarkModeToggle() { const scheme = usePdf((state) => state.colorScheme); const setScheme = usePdf((state) => state.setColorScheme); return ( ); } ``` Choose one owner for the scheme. When the `colorScheme` prop is supplied, it synchronizes the store back to that value; a store-only toggle will not override it permanently. ## Customize the palette `darkModeColors` accepts optional `background` and `foreground` colors. The defaults, exported as `DEFAULT_DARK_MODE_COLORS`, are `#141210` and `#eae6e0`. ```tsx {/* Your bounded Pages container */} ``` The background replaces white paper and the foreground replaces black text and line art. Omitted fields in a supplied `darkModeColors` prop resolve to the defaults. The prop controls the palette even if you toggle the scheme through the store. Without a palette prop, `setColorScheme("dark", { background, foreground })` updates it at runtime. Toggling the scheme without a new palette preserves the current palette. Pass resolved CSS colors such as hex or `rgb(...)`; `var(--token)` is not resolved by the canvas color mapper. If your theme stores colors in CSS variables, resolve them after the intended theme class has been applied. Reading computed styles during render can capture the previous theme's values. ## Match custom overlays `createDarkModeColorMap(colors)` returns a color-mapping function. Use it for application overlays that should follow the PDF palette: ```tsx title="themed-highlight-layer.tsx" "use client"; import { HighlightLayer, createDarkModeColorMap, usePdf } from "@anaralabs/lector"; export default function ThemedHighlightLayer() { const scheme = usePdf((state) => state.colorScheme); const colors = usePdf((state) => state.darkModeColors); const yellow = "#ffdf60"; const background = scheme === "dark" ? createDarkModeColorMap(colors)(yellow) : yellow; return ; } ``` Mount this under `Page` in place of `HighlightLayer`. If you use blend modes, test them against dark paper; a multiply blend chosen for white pages can make a highlight disappear on dark ones. ## Rendering behavior and limits Lector remaps vector fills and strokes and uses a custom PDF.js canvas factory for internal scratch canvases. The current implementation also detects mostly white, page-covering scanned paper and recolors qualifying scans. Photos, colorful scans, and embedded figures are generally preserved. Scan detection is heuristic, so test your own documents rather than assuming every raster page will change. Some mesh gradients retain their original colors, and transparency or soft-mask midtones can differ. DOM annotations and form widgets are separate from canvas recoloring; style them with your application's CSS. Passing a custom `CanvasFactory` through `documentOptions` replaces Lector's scratch-canvas recoloring integration. Remove older CSS inversion filters when using `colorScheme`; combining both approaches applies another transformation to the already recolored page. --- # Loading documents Load URLs and files, handle failures, and serve PDF.js assets from your own app. Source: https://anara.com/lector/docs/document-loading These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. `Root` accepts a URL string, a `URL`, an `ArrayBuffer`, a typed array, or a PDF.js `DocumentInitParameters` object as `source`. It reloads when the `source` reference changes. ## URLs and authenticated requests For a public file, use `source="/sample.pdf"`. Remote URLs must allow the browser's origin through CORS. Lector cannot bypass a server's CORS policy. For authenticated requests, keep the source object stable between renders: ```tsx title="authenticated-viewer.tsx" "use client"; import { CanvasLayer, Page, Pages, Root } from "@anaralabs/lector"; import { useMemo } from "react"; import "./pdf-setup"; export default function AuthenticatedViewer({ url, token, }: { url: string; token: string; }) { const source = useMemo( () => ({ url, httpHeaders: { Authorization: `Bearer ${token}` } }), [url, token], ); return ( ); } ``` For cookie-based cross-origin requests, PDF.js accepts `withCredentials: true`. Your server must allow credentialed requests from the specific app origin. See the [PDF.js loading parameters](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters). Do not create `source={{ url }}` or `new Uint8Array(...)` inline during render: a fresh object triggers another load. `documentOptions` is different: changes are read on the next document load and do not trigger one by themselves. ## Local files A `File` or `Blob` is not a direct `source` value. Create an object URL and release it when the file changes or the component unmounts: ```tsx title="local-file-viewer.tsx" "use client"; import { CanvasLayer, Page, Pages, Root, TextLayer } from "@anaralabs/lector"; import { useEffect, useState } from "react"; import "./pdf-setup"; export default function LocalFileViewer() { const [file, setFile] = useState(null); const [url, setUrl] = useState(null); useEffect(() => { if (!file) { setUrl(null); return; } const nextUrl = URL.createObjectURL(file); setUrl(nextUrl); return () => URL.revokeObjectURL(nextUrl); }, [file]); return ( <> {url && ( )} ); } ``` You can also read `await file.arrayBuffer()` and pass the bytes. PDF.js may transfer typed arrays to its worker, taking ownership of the buffer. Keep a separate copy if your app needs the original bytes or must retry a load. See [PDF.js binary data loading](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters). ## Show errors and retry `loader` is waiting content, not an error boundary. Without your own error UI, a failed load can leave it visible. `onError` receives `{ error, phase, source }`; `error` is `unknown`. Render failures outside `Root`, since its children are not mounted during loading. ```tsx title="resilient-viewer.tsx" "use client"; import { CanvasLayer, Page, Pages, Root, TextLayer } from "@anaralabs/lector"; import { useState } from "react"; import "./pdf-setup"; export default function ResilientViewer({ source }: { source: string }) { return ; } function DocumentSession({ source }: { source: string }) { const [attempt, setAttempt] = useState(0); const [failed, setFailed] = useState(false); if (failed) { return (

We couldn't open this PDF. Check the file or try again.

); } return ( Loading PDF…

} onError={({ error, phase }) => { console.error(`PDF failed during ${phase}`, error); setFailed(true); }} >
); } ``` | Phase | What failed | | --------------------- | --------------------------------------------------------------------------------------------- | | `pdfjs-load` | Importing the PDF.js runtime | | `document-load` | Loading or parsing the document, including worker initialization, network, or password errors | | `viewport-generation` | Resolving pages after the document loaded | `onDocumentLoad={({ proxy, source }) => …}` fires when PDF.js resolves the document, **before** Lector finishes generating page viewports. It is not a signal that canvases have painted. `onError` covers initialization failures; it does not catch every later canvas, text, or annotation rendering error. For a known PDF password, pass `documentOptions={{ password }}`. Lector does not expose a password-prompt callback. If credentials or the password change, reload using a new source reference or remount `Root` with a new `key`. ## Self-host PDF.js assets Lector supplies versioned jsDelivr URLs for auxiliary PDF.js resources by default. For offline or same-origin deployment, copy the resources from your installed `pdfjs-dist` package: ```bash mkdir -p public/pdfjs cp node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs public/pdfjs/ cp -R node_modules/pdfjs-dist/wasm public/pdfjs/ cp -R node_modules/pdfjs-dist/cmaps public/pdfjs/ cp -R node_modules/pdfjs-dist/standard_fonts public/pdfjs/ cp -R node_modules/pdfjs-dist/iccs public/pdfjs/ ``` Keep these assets synchronized with the dependency when building or deploying. Configure the worker as in [installation](https://anara.com/lector/docs/installation), then pass the auxiliary directories to `Root`: ```tsx ``` Include trailing slashes and any application path prefix. Image decoders matter for scanned PDFs; character maps and font data matter for documents with non-embedded or CJK fonts. Serve `.mjs` as JavaScript and `.wasm` as `application/wasm`, and check your deployment's worker and resource CSP rules if the browser blocks requests. `documentOptions` overrides both source-object options and Lector's defaults. Supplying your own `CanvasFactory` also replaces Lector's dark-mode scratch-canvas factory. --- # Installation Install compatible dependencies, configure the worker, and prepare a browser-only viewer. Source: https://anara.com/lector/docs/installation These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. ## Install the packages Use React 19 or later. The current Lector source expects `pdfjs-dist` `^5.5.207`; keep the PDF.js runtime and worker on the **same exact version**. ```bash npm install @anaralabs/lector pdfjs-dist@^5.5.207 ``` With pnpm, use `pnpm add @anaralabs/lector pdfjs-dist@^5.5.207`. Lector is an ES module package. Import it with `import`, and render the viewer in the browser. ## Configure the worker The worker parses PDFs off the main thread. Configure it **before mounting `Root`**, using the same legacy PDF.js build that Lector loads internally. The following setup works without a bundler-specific worker loader. From your application's directory, copy the worker into its public assets: ```bash mkdir -p public/pdfjs cp node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs public/pdfjs/ ``` Create `pdf-setup.ts` alongside your viewer: ```ts title="pdf-setup.ts" import { GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mjs"; import "pdfjs-dist/web/pdf_viewer.css"; GlobalWorkerOptions.workerSrc = "/pdfjs/pdf.worker.min.mjs"; ``` Import `./pdf-setup` in your viewer module. Recopy the worker whenever you upgrade PDF.js, ideally as part of your app's build. If the app is served below a path prefix, include that prefix in `workerSrc`. Confirm that the worker URL returns JavaScript, not your app's HTML fallback. ### Let your bundler emit the worker If your bundler supports package-relative asset URLs, you can replace the `workerSrc` assignment with: ```ts GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/legacy/build/pdf.worker.mjs", import.meta.url, ).toString(); ``` For Vite, an explicit asset import is another option: ```ts import workerUrl from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url"; GlobalWorkerOptions.workerSrc = workerUrl; ``` Use one approach. The public-file approach is useful when your framework cannot resolve the package-relative URL. Check both development and production builds. ## Next.js: keep the viewer out of server rendering A `"use client"` directive alone does not prevent a component from being prerendered on the server. Put your viewer and its `./pdf-setup` import in `pdf-viewer.tsx`, then load it from a separate Client Component: ```tsx title="pdf-viewer-client.tsx" "use client"; import dynamic from "next/dynamic"; const PDFViewer = dynamic(() => import("./pdf-viewer"), { ssr: false, loading: () =>

Preparing PDF viewer…

, }); export default PDFViewer; ``` Your route can import `PDFViewer` from this wrapper. Keep the PDF.js setup import inside the dynamically loaded viewer, so it is not evaluated on the server. In the Pages Router, import the PDF.js global stylesheet from `pages/_app.tsx` instead of `pdf-setup.ts`. Next.js requires `ssr: false` to live in a Client Component. See [Next.js lazy loading](https://nextjs.org/docs/app/guides/lazy-loading#skipping-ssr) for the framework's rules. ## Styles and PDF assets `pdfjs-dist/web/pdf_viewer.css` positions text and annotations over the canvas. Without it, text selection and links can be misplaced even when the PDF image looks correct. The examples use inline styles for their layout; they do not require an application UI kit. Some optional Lector components, including the colored-highlight tools, use Tailwind utility classes internally. If you use those components, include the library's distributed JavaScript in your Tailwind content sources, or supply equivalent styles. The worker is separate from PDF.js's fonts, character maps, image decoders, and color profiles. Lector defaults these auxiliary resources to versioned jsDelivr URLs. To serve all PDF assets from your own origin, follow [self-hosting PDF.js assets](https://anara.com/lector/docs/document-loading#self-host-pdfjs-assets). ## Verify the setup Put a PDF at `public/sample.pdf` and open `/sample.pdf` directly to check that it is served correctly. Then follow [your first viewer](https://anara.com/lector/docs/basic-usage). You should see pages inside a scrollable container and be able to select text in a text-based PDF. If you see an empty viewer or a worker error, use the [troubleshooting guide](https://anara.com/lector/docs/troubleshooting). --- # Troubleshooting Diagnose worker errors, blank pages, misplaced layers, reload loops, and missing search results. Source: https://anara.com/lector/docs/troubleshooting These docs describe the source deployed with this site, tracking main rather than a versioned npm release. Check your installed @anaralabs/lector and pdfjs-dist versions before applying examples. Start with the [basic example](https://anara.com/lector/docs/code/basic), a local PDF, and the browser's Console and Network tabs. Fix the first failed request or exception before adding optional layers back. ## The viewer is blank Check these in order: 1. Open the PDF's URL directly. It must return PDF bytes, not a login page, a 404, or your app's HTML shell. 2. Inspect `Root` and the parent of `Pages`. They need a nonzero, bounded height. With a toolbar, use [the flex layout](https://anara.com/lector/docs/basic-usage#give-the-pages-room-to-scroll). 3. Confirm `Pages` contains one `Page` template with a `CanvasLayer`. 4. Check the worker request and any failed `.wasm`, character-map, or font requests. 5. Add [`onError`](https://anara.com/lector/docs/document-loading#show-errors-and-retry) so a load failure is not hidden behind the loader. If ordinary PDFs work but scans are blank, check image-decoder requests. Lector's default decoders come from jsDelivr; blocked CDN requests can prevent JPEG2000 or JBIG2 image content from rendering. [Self-host the auxiliary assets](https://anara.com/lector/docs/document-loading#self-host-pdfjs-assets) if needed. ## No workerSrc, fake-worker, or version-mismatch errors Configure `GlobalWorkerOptions` from `pdfjs-dist/legacy/build/pdf.mjs`, the same build Lector imports. Configure it before `Root` mounts. | Symptom | Check | | --------------------------------------- | -------------------------------------------------------------------------------- | | Worker URL returns 404 | Public path, deployment prefix, and copied filename | | Worker URL returns HTML | SPA fallback or route interception | | Browser refuses the worker | Response MIME type and the browser's CSP error | | API version differs from worker version | Recopy the worker from the installed package; invalidate stale deployment caches | | Multiple PDF.js versions installed | Inspect `pnpm why pdfjs-dist` or `npm ls pdfjs-dist` | Keep the worker and auxiliary assets in sync when upgrading. See [worker setup](https://anara.com/lector/docs/installation#configure-the-worker). ## DOMMatrix, window, or document is not defined PDF.js or viewer code is being evaluated on the server. In Next.js, use the [client wrapper with `ssr: false`](https://anara.com/lector/docs/installation#nextjs-keep-the-viewer-out-of-server-rendering), and keep `pdf-setup` inside the dynamically imported module. `"use client"` alone does not disable prerendering. ## The document keeps reloading Check whether `source` is a new object on every render: ```tsx // A new source reference on every render: // A stable string for a public URL: ``` Memoize objects containing headers or other loading parameters. Keep binary data in state or a stable reference. Avoid changing the viewer's React `key` unless you intend to reset its state. ## Text, links, or highlights are misplaced Import `pdfjs-dist/web/pdf_viewer.css`. Check that your CSS does not override PDF.js text positioning, page dimensions, or scale variables. Remove application-level transforms on `Pages`, `Page`, and the layers. For custom highlights, distinguish scale-1 page pixels, percentages, screen coordinates, and raw PDF coordinates. A percent rectangle needs `type: "percent"` on the rectangle itself. See [highlight coordinates](https://anara.com/lector/docs/code/highlight#coordinate-contract). ## Hooks throw or controls do nothing Viewer hooks must run in descendants of `Root`. Page-specific hooks and layers need a `Page` ancestor too. A hook called before returning `` is outside the provider. Page navigation requires a mounted `Pages` container. Page numbers start at 1. `NextPage` and `PreviousPage` are placeholders; use [custom controls](https://anara.com/lector/docs/code/page-navigation). `setCurrentPage` changes state without scrolling; use `jumpToPage` instead. ## Search has no results or fewer results than expected Mount `Search` under `Root` to populate the text index before using `useSearch`. Check whether the PDF has embedded text; image-only scans require OCR outside Lector. Visually adjacent words may be extracted with unexpected spacing or order. The result limit is split between exact and fuzzy groups, so it is not an exact-match count. See [search limits](https://anara.com/lector/docs/code/search#search-options-and-results) before building pagination. Search runs across the whole text index, so it can take time on long PDFs even with a small result limit. ## Form values disappear from a DOM export Offscreen pages are virtualized and their form controls may be unmounted. `FormData` only sees mounted HTML controls. Use the PDF.js document's [saveDocument()](https://anara.com/lector/docs/code/pdf-form) for an edited PDF and annotation storage for document-level form state. ## Scrolling or zooming is slow Reproduce with a representative PDF on the target device. Compare canvas-only rendering with text, annotation, search, and thumbnail features added one at a time. Keep `virtualizerOptions.overscan` small; more pages mean more rendering work. Search extraction visits every page independently of page virtualization. Use narrow `usePdf` selectors so unrelated state changes do not rerender a whole toolbar or sidebar. Avoid rebuilding the source object during interactions. Confirm the problem occurs in a production build as well as development before reporting performance measurements. ## Report an actionable issue Include the installed Lector, React, and PDF.js versions; browser and framework versions; a minimal reproduction; the exact error and failed request status; and expected versus actual behavior. Note whether it happens in development, production, or both. Attach a shareable PDF or a reduced document that reproduces the problem. Remove private content and request credentials. A page count alone cannot describe the fonts, images, annotations, or structure that trigger PDF rendering bugs. [Open an issue](https://github.com/anaralabs/lector/issues/new).