---
title: "How to build area annotations with canvas capture in PDF.js"
canonical_url: "https://www.nutrient.io/blog/pdfjs-area-annotations-canvas-capture/"
md_url: "https://www.nutrient.io/blog/pdfjs-area-annotations-canvas-capture.md"
last_updated: "2026-06-24T12:54:18.087Z"
description: "Build area annotations in PDF.js: pointer tracking, canvas-region capture, PDF coordinate conversion, and draggable overlays with React."
---

**TL;DR**

- Capture pointer drag/release on a fullscreen overlay and translate the screen rectangle into PDF coordinates via the page’s `PageViewport`.

- Crop the rendered canvas region with `drawImage` and `toDataURL` to store the area as a JPEG or PNG.

- Render annotations back over the page with `react-rnd`, recropping on drag/resize, and recalculating positions on zoom/rotation.

## Prerequisites

This guide assumes a working PDF.js viewer (`PDFViewer` + `EventBus` + a loaded `pdfDocument`). If you don’t have that yet, start with our guide on [how to set up a custom PDF.js viewer in React](https://www.nutrient.io/blog/pdfjs-react-viewer-setup.md).

You’ll also need:

- `pdfjs-dist` 4.x or later

- The `react-rnd` package (`npm install react-rnd`) for draggable/resizable overlays

- The `screenToPdf`/`pdfToScreen`/`nodeLocationToScaled` helpers from [PDF.js coordinate systems: PDF to screen](https://www.nutrient.io/blog/pdfjs-coordinate-systems-pdf-to-screen.md) — this post uses them but doesn’t redefine them.

## Architecture

```

User selects area tool
  -> Pointer tracking overlay captures drag start/end
  -> Selection box renders during drag
  -> On release: convert screen rectangle to PDF coordinates
  -> Capture canvas region as an image (JPEG/PNG)
  -> Save annotation with coordinates + image data
  -> Render as draggable/resizable overlay

```

## Step 1: Pointer tracking overlay

Create a transparent overlay that captures pointer events when the area tool is active:

```tsx

function PointerArea({ disabled, selectionBox, style, shouldStart, onStart, onEnd }) {
  const [isDrawing, setIsDrawing] = React.useState(false);
  const [origin, setOrigin] = React.useState(null);
  const [current, setCurrent] = React.useState(null);
  const startElementRef = React.useRef(null);

  const handlePointerDown = (e) => {
    if (disabled) return;
    if (!shouldStart(e)) return;

    startElementRef.current = e.target;
    setOrigin({ x: e.clientX, y: e.clientY });
    setCurrent({ x: e.clientX, y: e.clientY });
    setIsDrawing(true);
    onStart?.();
  };

  const handlePointerMove = (e) => {
    if (!isDrawing) return;
    setCurrent({ x: e.clientX, y: e.clientY });
  };

  const handlePointerUp = (e) => {
    if (!isDrawing) return;
    setIsDrawing(false);

    onEnd(startElementRef.current, {
      clientOrigin: [origin.x, origin.y],
      clientTarget: [e.clientX, e.clientY],
    });
  };

  return (
    <div
      style={{ position: "fixed", inset: 0, zIndex: 100 }}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onPointerUp={handlePointerUp}
    >
      {isDrawing && selectionBox && (
        <div
          style={{...style,
            position: "absolute",
            left: Math.min(origin.x, current.x),
            top: Math.min(origin.y, current.y),
            width: Math.abs(current.x - origin.x),
            height: Math.abs(current.y - origin.y),
          }}
        />
      )}
    </div>
  );
}

```

## Step 2: Handle selection completion

When the pointer is released, determine which page was selected and convert coordinates:

```tsx

const handlePointerEnd = (startElement, origins) => {
  const page = getPageFromElement(startElement);
  if (!page) return;

  const pageBounds = page.canvasWrapper.getBoundingClientRect();

  const clientLeft = Math.min(origins.clientOrigin[0], origins.clientTarget[0]);
  const clientTop = Math.min(origins.clientOrigin[1], origins.clientTarget[1]);

  const canvasLeft = clientLeft - pageBounds.left;
  const canvasTop = clientTop - pageBounds.top;
  const width = Math.abs(origins.clientOrigin[0] - origins.clientTarget[0]);
  const height = Math.abs(origins.clientOrigin[1] - origins.clientTarget[1]);

  // Skip tiny selections (accidental clicks).
  if (width * height <= 100) return;

  const viewport = viewer.getPageView(page.number - 1).viewport;

  // Convert to PDF coordinates for storage.
  const pdfCoords = screenToPdf(
    { left: canvasLeft, top: canvasTop, width, height, pageNumber: page.number },
    viewport,
  );

  // Capture the canvas region as an image.
  const canvas = viewer.getPageView(page.number - 1).canvas;
  const imageContent = getAreaAsImage(canvas, {
    left: canvasLeft, top: canvasTop, width, height,
  });

  // Save annotation.
  onCreateAreaAnnotation(
    color,
    page.number,
    [pdfCoords.x1, pdfCoords.y1, pdfCoords.x2, pdfCoords.y2],
    imageContent,
  );
};

```

## Step 3: Capturing canvas regions

PDF.js renders pages on `<canvas>` elements. You can extract any rectangular region:

```tsx

function getAreaAsImage(canvas, position) {
  const { left, top, width, height } = position;

  const newCanvas = document.createElement("canvas");
  newCanvas.width = width;
  newCanvas.height = height;

  const ctx = newCanvas.getContext("2d");
  if (!ctx ||!canvas) return "";

  // PDF.js renders at its own output scale, which is usually but not always
  // `window.devicePixelRatio`. Read the real scale from the rendered canvas.
  const dpr = canvas.width / canvas.clientWidth;

  ctx.drawImage(
    canvas,
    left * dpr,      // source x (scaled for DPI).
    top * dpr,       // source y.
    width * dpr,     // source width.
    height * dpr,    // source height.
    0, 0,            // destination origin.
    width, height,   // destination size (CSS pixels).
  );

  return newCanvas.toDataURL("image/jpeg");
}

```

**DPI matters!** PDF.js renders canvases at its own output scale, which is usually but not always equal to `window.devicePixelRatio` (PDF.js caps the scale and lets configurations override it). Derive the real scale from `canvas.width / canvas.clientWidth` rather than reading `devicePixelRatio` directly.

## Step 4: Render as draggable/resizable box

Use `react-rnd` to render area annotations that users can drag and resize:

```tsx

import { Rnd } from "react-rnd";

function AreaAnnotation({ annotation, viewport, page }) {
  const ref = React.useRef(null);
  const positionRef = React.useRef(null);

  // Update position when viewport changes (zoom/rotation).
  React.useEffect(() => {
    const pdfCoords = nodeLocationToScaled({
      page: annotation.page,
      rect: annotation.rect,
    });
    const position = pdfToScreen(pdfCoords, viewport);
    positionRef.current = position;

    ref.current?.updatePosition({ x: position.left, y: position.top });
    ref.current?.updateSize({ width: position.width, height: position.height });
  }, [annotation.rect, viewport]);

  const handleChangePosition = (newPosition) => {
    // Convert back to PDF coordinates.
    const pdfCoords = screenToPdf(
      {...positionRef.current,...newPosition },
      viewport,
    );
    const location = {
      page: pdfCoords.pageNumber,
      rect: [pdfCoords.x1, pdfCoords.y1, pdfCoords.x2, pdfCoords.y2],
    };

    // Recapture the canvas region with new bounds.
    const canvas = viewer.getPageView(page - 1).canvas;
    const newImage = getAreaAsImage(canvas, {...positionRef.current,...newPosition });

    // Save updated annotation.
    saveAnnotation(annotation.id, { rect: location.rect, image_content: newImage });
  };

  return (
    <Rnd
      ref={ref}
      style={{
        position: "absolute",
        background: "rgba(255, 94, 94, 0.4)",
        borderRadius: "8px",
        cursor: "pointer",
      }}
      onDragStop={(_, data) => {
        handleChangePosition({ top: data.y, left: data.x });
      }}
      onResizeStop={(_, __, ref, ___, position) => {
        handleChangePosition({
          top: position.y,
          left: position.x,
          width: ref.offsetWidth,
          height: ref.offsetHeight,
        });
      }}
    />
  );
}

```

## Step 5: Disable text selection during drawing

When the area tool is active, prevent text selection from interfering:

```tsx

function toggleDisableSelection(viewer, flag) {
  viewer.viewer?.classList.toggle("disableSelection", flag);
}

```

```css.disableSelection {
  user-select: none;
  -webkit-user-select: none;
}.disableTouchAction {
  touch-action: none;
}

```

Enable `disableSelection` on pointer down, and disable on pointer up.

## Minimum size threshold

Ignore accidental clicks by checking the area:

```tsx

if (width * height <= 100) return; // Less than ~10x10 pixels.

```

## Key points

- Use a fullscreen overlay to capture pointer events outside the PDF container.

- Subtract `canvasWrapper.getBoundingClientRect()` to get page-relative coordinates.

- Derive the output scale from `canvas.width / canvas.clientWidth` rather than `window.devicePixelRatio` — PDF.js caps the render scale and configurations can override it.

- `canvas.toDataURL("image/jpeg")` gives you a base64 string for storage (use `"image/png"` for lossless capture).

- Recapture the canvas region when the user drags/resizes the annotation.

- `react-rnd` provides drag and resize with a clean React API.

- Update the `Rnd` position imperatively via `ref.updatePosition()` when the viewport changes.

## How Nutrient Web SDK handles this

Nutrient Web SDK ships `RectangleAnnotation` with drag, resize, and draw-on-page interactions wired into the default UI. The rectangle tool appears in the toolbar without any extra code. Use the programmatic API when creating annotations from code — import flows, automation, server-driven annotations:

```js

const annotation = new NutrientViewer.Annotations.RectangleAnnotation({
  pageIndex: 0,
  boundingBox: new NutrientViewer.Geometry.Rect({
    left: 50,
    top: 100,
    width: 300,
    height: 200,
  }),
  strokeColor: NutrientViewer.Color.RED,
  strokeWidth: 2,
});
await instance.create(annotation);

```

To export the pixels under a rectangle as an image — useful for callouts, off-page references, or sharing a region without the source PDF — use `renderPageAsArrayBuffer` to grab the rendered bytes for the given area. Refer to our guide on how to [render visible area in current page](https://www.nutrient.io/guides/web/knowledge-base/render-visible-area-in-current-page.md) for the full pattern.

For other vector overlays, the SDK also supports `EllipseAnnotation`, `PolygonAnnotation`, and `PolylineAnnotation` — all draggable and resizable by default.

## FAQ

#### JPEG or PNG for the captured region?

JPEG keeps file size small for photographs and screenshots of dense pages, but it compresses text and line art poorly. PNG is lossless and noticeably better for scans, diagrams, or text-heavy regions — at the cost of bigger payloads. If you store many annotations per document, default to JPEG and switch to PNG selectively.

#### Why use <code>canvas.width / canvas.clientWidth</code> instead of <code>window.devicePixelRatio</code>?

PDF.js doesn’t always render at `devicePixelRatio` — it caps the output scale on huge pages and respects configurations that override it. The actual rendered resolution is whatever’s in the canvas’s intrinsic `width`/`height`, so deriving the scale from those gives you the correct pixel ratio every time.

#### How do I support touch input?

Pointer events already cover touch, mouse, and pen — `pointerdown`/`pointermove`/`pointerup` fire for all three. Add `touch-action: none` (the `.disableTouchAction` class in the CSS section above) to the drawing surface to stop the browser from interpreting the drag as a scroll.

#### What happens when the page is rotated?

`PageViewport.convertToPdfPoint()` returns coordinates in the PDF’s own coordinate space, so a rotated page captures correctly. The catch is that `convertToPdfPoint` can return `x1 > x2` or `y1 > y2` for rotated viewports — normalize with `Math.min`/`Math.max` before storing.

#### Should I store the captured image or rerender it on demand?

Rerendering is cheaper at write time and easier to keep current when the underlying PDF changes, but requires the original PDF to be available every time you display the annotation. Storing the cropped image (typical pattern in collaborative apps where the captured area is the artifact) costs storage but makes the annotation portable.

Area annotations take five coordinated pieces: a pointer-tracking overlay, coordinate conversion, canvas DPI scaling, image capture, and a drag-resize library. The individual steps are manageable — the overhead is keeping them in sync as zoom, rotation, and viewport changes update the layout.

---

_See [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) for built-in image and vector annotations without the canvas wiring, or follow the [migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) to switch from PDF.js. [Talk to Sales](https://www.nutrient.io/contact-sales/?=sdk) about your requirements._
---

## Related pages

- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Viewer](/blog/document-viewer.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [or](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [or](/blog/how-to-convert-html-to-pdf-using-react.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [How To Generate Pdf From Html With Nodejs](/blog/how-to-generate-pdf-from-html-with-nodejs.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [Html In Pdf Format](/blog/html-in-pdf-format.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Process Flows](/blog/process-flows.md)
- [or](/blog/sample-blog-updated.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [Why Pdfium Is A Trusted Platform For Pdf Rendering](/blog/why-pdfium-is-a-trusted-platform-for-pdf-rendering.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)

