---
title: "Measurements — React"
description: "Measure distances, perimeters and areas on drawings and plans at a real-world scale, and calibrate a page from a known length."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/annotations/measurements"
---

# Measurements

On a floor plan or a technical drawing, people want real sizes: how long this wall is, how big this
room. The measurement plugin adds the tools for it, and keeps each page's scale, so a line of 3
centimeters on the page reads as 3 meters in the room. Register `measurementPlugin()` next to
`interactionPlugin()` and `annotationPlugin()`, and turn on a tool:

**`tool.tsx`**

```tsx
import { useInteraction } from '@embedpdf/react/interaction';

export function MeasureButton() {
  const interaction = useInteraction();

  return <button onClick={() => interaction.activateTool('distance')}>Measure</button>;
}
```

| Tool        | Measures                              | How                                    |
| :---------- | :------------------------------------ | :------------------------------------- |
| `distance`  | The length of a line                  | Drag from one point to the other       |
| `perimeter` | The length of a path of several lines | Click each point, double-click to end  |
| `area`      | The area of a shape                   | Click each corner, double-click to end |

Each measurement is an annotation with its value as a label, so it shows the same in Acrobat and other
PDF apps, and you can select, move and restyle it like any annotation. The example measures the
cover's width at 1:100; pick a tool and measure something else:

**`basic.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
  interactionPlugin,
  useInteraction,
  useInteractionState,
} from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import { measurementPlugin, useMeasurement, usePageScale } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  measurementPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const TOOLS = [
  { id: 'pointer', label: 'Select' },
  { id: 'distance', label: 'Distance' },
  { id: 'perimeter', label: 'Perimeter' },
  { id: 'area', label: 'Area' },
];

function MeasureToolbar() {
  const interaction = useInteraction();
  const { activeToolId } = useInteractionState();
  const measurement = useMeasurement();
  const scale = usePageScale(0); // the cover's scale
  const started = useRef(false);

  // On load: the cover at 1:100, its width measured along the top, and the distance tool on.
  useEffect(() => {
    if (!scale.ready || started.current) return;
    started.current = true;
    void measurement
      .setPreset(0, 'metric-100')
      .then(() =>
        measurement.createMeasurement({
          kind: 'distance',
          page: 0,
          points: [
            { x: 30, y: 28 },
            { x: 582, y: 28 },
          ],
        }),
      )
      .then(() => interaction.activateTool('distance'));
  }, [measurement, interaction, scale.ready]);

  return (
    <div className="toolbar">
      <div className="segmented" role="group" aria-label="Tool">
        {TOOLS.map((tool) => (
          <button
            key={tool.id}
            type="button"
            aria-pressed={activeToolId === tool.id}
            onClick={() => interaction.activateTool(tool.id)}
          >
            {tool.label}
          </button>
        ))}
      </div>
      <span className="spacer" />
      <output className="badge">
        Scale {scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MeasureToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`basic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* One choice of a few: the pressed button is the current one */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 11px;
}

.segmented button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.segmented button:hover {
  color: var(--ink);
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(7 32 76 / 0.1);
}

.segmented button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* A fact about the page, such as its scale */
.badge {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 11px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Set the scale

A page starts at the default scale, 1:1 in meters, unless the PDF already has one. Most plans say
their scale, such as 1:100. Pick it from a list:

```ts
measurement.listPresets(); // 1:1, 1:10, 1:20, 1:50, 1:100, 1:200, 1/4 in = 1 ft, 1/8 in = 1 ft
await measurement.setPreset([page.ref], 'metric-100');
await measurement.setPreset('all', 'metric-100'); // every page
```

Measurements already on the page are recalculated for the new scale. Change the scale, the units
or the precision, and the two measurements follow:

**`scale.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
  measurementPlugin,
  useMeasurement,
  useMeasurementState,
  usePageScale,
} from '@embedpdf/react/measurement';
import type { AreaUnit, LengthUnit } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';

import './scale.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  measurementPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Precision is steps per unit: 100 shows two decimals.
const PRECISIONS = [1, 10, 100, 1000];

// The cover's empty lower half, where the measurements go.
const LOWER_HALF = { x: 40, y: 560, width: 532, height: 210 };

// On load: a distance and an area on the cover, at 1:100, scrolled into view.
function MeasureCover() {
  const measurement = useMeasurement();
  const stage = useStage();
  const ready = usePageScale(0).ready;
  const started = useRef(false);

  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void measurement.setPreset(0, 'metric-100').then(() =>
      Promise.all([
        measurement.createMeasurement({
          kind: 'distance',
          page: 0,
          points: [
            { x: 60, y: 740 },
            { x: 550, y: 740 },
          ],
        }),
        measurement.createMeasurement({
          kind: 'area',
          page: 0,
          points: [
            { x: 60, y: 580 },
            { x: 260, y: 580 },
            { x: 260, y: 700 },
            { x: 60, y: 700 },
          ],
        }),
      ]).then(() => stage.reveal(0, { rect: LOWER_HALF })),
    );
  }, [measurement, stage, ready]);

  return null;
}

function ScaleToolbar() {
  const measurement = useMeasurement();
  const { busy } = useMeasurementState();
  const scale = usePageScale(0);
  const [preset, setPreset] = useState('metric-100');
  const [unit, setUnit] = useState<LengthUnit>('m');
  const [areaUnit, setAreaUnit] = useState<AreaUnit>('m2');
  const [precision, setPrecision] = useState(100);

  // Every change recalculates the measurements already on the page.
  return (
    <div className="toolbar">
      <label className="label">
        Scale
        <select
          className="field"
          value={preset}
          disabled={busy}
          onChange={(event) => {
            setPreset(event.target.value);
            void measurement.setPreset(0, event.target.value);
          }}
        >
          {measurement.listPresets().map((choice) => (
            <option key={choice.id} value={choice.id}>
              {choice.label}
            </option>
          ))}
        </select>
      </label>
      <label className="label">
        Length
        <select
          className="field"
          value={unit}
          disabled={busy}
          onChange={(event) => {
            const next = event.target.value as LengthUnit;
            setUnit(next);
            void measurement.setUnit(0, next);
          }}
        >
          {measurement.listUnits().map((choice) => (
            <option key={choice} value={choice}>
              {choice}
            </option>
          ))}
        </select>
      </label>
      <label className="label">
        Area
        <select
          className="field"
          value={areaUnit}
          disabled={busy}
          onChange={(event) => {
            const next = event.target.value as AreaUnit;
            setAreaUnit(next);
            void measurement.setAreaUnit(0, next);
          }}
        >
          {measurement.listAreaUnits().map((choice) => (
            <option key={choice} value={choice}>
              {choice}
            </option>
          ))}
        </select>
      </label>
      <label className="label">
        Steps
        <select
          className="field"
          value={precision}
          disabled={busy}
          onChange={(event) => {
            const next = Number(event.target.value);
            setPrecision(next);
            void measurement.setPrecision(0, next);
          }}
        >
          {PRECISIONS.map((choice) => (
            <option key={choice} value={choice}>
              {choice === 1 ? 'Whole' : `1/${choice}`}
            </option>
          ))}
        </select>
      </label>
      <span className="spacer" />
      <output className="badge">
        {scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MeasureCover />
        <ScaleToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`scale.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* A text box or a menu in the toolbar */
.field {
  appearance: none;
  min-width: 0;
  box-sizing: border-box;
  min-height: 34px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

select.field {
  appearance: auto;
  padding-right: 6px;
  cursor: pointer;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}

/* A fact about the page, such as its scale */
.badge {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 11px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Calibrate from a known length

When the scale isn't printed, measure something you know, such as a door that's 90 centimeters wide:

```ts
measurement.startCalibration(); // the next line the user draws is the known length
```

The plugin asks your UI for the real length, through `useMeasurementState().calibrationRequest`:

**`calibration.tsx`**

```tsx
import { useMeasurement, useMeasurementState } from '@embedpdf/react/measurement';

import { LengthPrompt } from './length-prompt';

export function CalibrationDialog() {
  const measurement = useMeasurement();
  const { calibrationRequest } = useMeasurementState();
  if (!calibrationRequest) return null;

  const apply = (value: number) =>
    measurement.calibrate({ ...calibrationRequest, distance: { value, unit: 'cm' } });

  return <LengthPrompt onSubmit={apply} onCancel={() => measurement.dismissCalibration()} />;
}
```

`calibrate()` sets the page's scale from the two points and the length you give. Pass
`{ applyTo: 'all' }` to use the same scale on every page.

**`calibrate.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin, useInteraction } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
  measurementPlugin,
  useMeasurement,
  useMeasurementState,
  usePageScale,
} from '@embedpdf/react/measurement';
import type { LengthUnit } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';

import './calibrate.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  measurementPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

function Calibration() {
  const measurement = useMeasurement();
  const interaction = useInteraction();
  const { calibrationRequest, busy } = useMeasurementState();
  const scale = usePageScale(0);
  const [length, setLength] = useState('20');
  const [unit, setUnit] = useState<LengthUnit>('cm');
  const started = useRef(false);

  // On load: calibrating, so the next line you drag is the known length.
  useEffect(() => {
    if (!scale.ready || started.current) return;
    started.current = true;
    if (measurement.canCalibrate()) measurement.startCalibration();
  }, [measurement, scale.ready]);

  // The plugin asks for the real length of the line you drew.
  if (calibrationRequest) {
    const value = Number(length);
    return (
      <form
        className="toolbar"
        onSubmit={(event) => {
          event.preventDefault();
          void measurement
            .calibrate({ ...calibrationRequest, distance: { value, unit } })
            .then(() => interaction.activateTool('distance'));
        }}
      >
        <span className="readout">That line is</span>
        <input
          className="field length"
          type="number"
          min="0"
          step="any"
          aria-label="Its real length"
          value={length}
          onChange={(event) => setLength(event.target.value)}
        />
        <select
          className="field"
          aria-label="Unit"
          value={unit}
          onChange={(event) => setUnit(event.target.value as LengthUnit)}
        >
          {measurement.listUnits().map((choice) => (
            <option key={choice} value={choice}>
              {choice}
            </option>
          ))}
        </select>
        <button type="submit" className="button" disabled={busy || !(value > 0)}>
          Set the scale
        </button>
        <button type="button" className="button" onClick={() => measurement.dismissCalibration()}>
          Cancel
        </button>
      </form>
    );
  }

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!measurement.canCalibrate()}
        onClick={() => measurement.startCalibration()}
      >
        Calibrate
      </button>
      <output className="readout">
        Drag along something you know the length of, then measure with the new scale
      </output>
      <span className="spacer" />
      <output className="badge">
        {scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <Calibration />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`calibrate.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* A text box or a menu in the toolbar */
.field {
  appearance: none;
  min-width: 0;
  box-sizing: border-box;
  min-height: 34px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

select.field {
  appearance: auto;
  padding-right: 6px;
  cursor: pointer;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}

/* A fact about the page, such as its scale */
.badge {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 11px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The known length: a short number */
.field.length {
  width: 88px;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Units and precision

```ts
await measurement.setUnit([page.ref], 'ft');
await measurement.setAreaUnit([page.ref], 'ft2'); // or 'm2', 'ha', 'acre'
await measurement.setPrecision([page.ref], 100); // two decimals
```

`listUnits()` and `listAreaUnits()` give you the choices for a menu. Precision is 1, 10, 100 and so on:
the number of steps per unit.

## Read a measurement

**`readout.tsx`**

```tsx
import type { AnnotationRef } from '@embedpdf/react/annotation';
import { useMeasurementReadout } from '@embedpdf/react/measurement';

export function MeasurementLabel({ annotation }: { annotation: AnnotationRef }) {
  const readout = useMeasurementReadout(annotation);
  // { kind: 'distance', value: 3.42, label: '3.42 m' }

  return <span>{'unavailable' in readout ? '—' : readout.label}</span>;
}
```

`label` is the text shown on the page, in the page's units. Without a scale to compute from, a
readout says why: `{ unavailable: 'no-scale' }`.

**`readout.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationKey,
  annotationPlugin,
  useAnnotation,
  useAnnotationList,
} from '@embedpdf/react/annotation';
import type { Annotation } from '@embedpdf/react/annotation';
import {
  measurementPlugin,
  useMeasurement,
  useMeasurementReadout,
  usePageScale,
} from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';

import './readout.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  measurementPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// The cover's empty lower half, where the measurements go.
const LOWER_HALF = { x: 40, y: 560, width: 532, height: 210 };

// On load: the cover at 1:50, and one measurement of each kind on it, scrolled into view.
function MeasureCover() {
  const measurement = useMeasurement();
  const stage = useStage();
  const ready = usePageScale(0).ready;
  const started = useRef(false);

  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void measurement.setPreset(0, 'metric-50').then(() =>
      Promise.all([
        measurement.createMeasurement({
          kind: 'distance',
          page: 0,
          points: [
            { x: 60, y: 740 },
            { x: 550, y: 740 },
          ],
        }),
        measurement.createMeasurement({
          kind: 'perimeter',
          page: 0,
          points: [
            { x: 60, y: 580 },
            { x: 160, y: 640 },
            { x: 260, y: 590 },
          ],
        }),
        measurement.createMeasurement({
          kind: 'area',
          page: 0,
          points: [
            { x: 80, y: 650 },
            { x: 250, y: 650 },
            { x: 250, y: 710 },
            { x: 80, y: 710 },
          ],
        }),
      ]).then(() => stage.reveal(0, { rect: LOWER_HALF })),
    );
  }, [measurement, stage, ready]);

  return null;
}

// A measurement is a line, polyline or polygon with a dimension intent.
const isMeasurement = (annotation: Annotation) =>
  'intent' in annotation && !!annotation.intent?.endsWith('-dimension');

function Readout({ annotation }: { annotation: Annotation }) {
  const readout = useMeasurementReadout(annotation.ref);
  const { selection } = useAnnotation();

  return (
    <li>
      <button type="button" className="row" onClick={() => selection.set([annotation.ref])}>
        <span className="kind">{'unavailable' in readout ? 'No scale' : readout.kind}</span>
        <span className="value">{'unavailable' in readout ? '—' : readout.label}</span>
      </button>
    </li>
  );
}

function Readouts() {
  const measurements = useAnnotationList({ pages: [0] }).filter(isMeasurement);

  return (
    <ul className="readouts" aria-label="Measurements on the cover">
      {measurements.map((annotation) => (
        <Readout key={annotationKey(annotation.ref)} annotation={annotation} />
      ))}
    </ul>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MeasureCover />
        <div className="viewer">
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <AnnotationLayer />
              </>
            )}
          </Stage>
          <Readouts />
        </div>
      </DocumentGate>
    </Viewer>
  );
}
```

**`readout.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* The pages and the measurements: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.viewer .stage {
  flex: 1 1 100%;
  box-sizing: border-box;
  height: 340px;
}

.readouts {
  flex: 1 1 100%;
  box-sizing: border-box;
  max-height: 220px;
  overflow-y: auto;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 1px 2px var(--shadow);
}

@container (min-width: 620px) {
  .viewer .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 420px;
  }

  .readouts {
    flex: 0 0 220px;
    max-height: 420px;
  }
}

/* One measurement: what it is, and its value */
.row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  width: 100%;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 9px;
  padding: 9px 10px;
  cursor: pointer;
}

.row:hover {
  background: var(--tint);
}

.row:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.kind {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.value {
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

To measure without drawing anything, for a tooltip that follows the pointer:

```ts
measurement.measureDistance(page.ref, from, to); // { value, label }
measurement.measureArea(page.ref, corners);
```

`usePageScale(page.ref)` gives you a page's scale, for a scale bar or a "1:100" badge.

## Measure from code

```ts
const { annotation } = await measurement.createMeasurement({
  kind: 'area',
  page: page.ref,
  points: [
    { x: 100, y: 100 },
    { x: 300, y: 100 },
    { x: 300, y: 250 },
  ],
});
```

It makes the same annotation the tool would, with the page's scale and the tool's style.

**`from-code.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin, useAnnotationList } from '@embedpdf/react/annotation';
import { measurementPlugin, useMeasurement, usePageScale } from '@embedpdf/react/measurement';
import type { MeasurementKind } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';

import './from-code.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  measurementPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Points on the cover's empty lower half, in page coordinates: two for a
// distance, the corners otherwise.
const SHAPES: Record<MeasurementKind, { x: number; y: number }[]> = {
  distance: [
    { x: 60, y: 750 },
    { x: 550, y: 750 },
  ],
  perimeter: [
    { x: 70, y: 600 },
    { x: 150, y: 650 },
    { x: 250, y: 600 },
  ],
  area: [
    { x: 70, y: 670 },
    { x: 250, y: 670 },
    { x: 250, y: 730 },
    { x: 70, y: 730 },
  ],
};

// The cover's empty lower half, where the measurements go.
const LOWER_HALF = { x: 40, y: 560, width: 532, height: 210 };

function MeasureButtons() {
  const measurement = useMeasurement();
  const stage = useStage();
  const scale = usePageScale(0);
  const count = useAnnotationList({ pages: [0] }).filter(
    (annotation) => 'intent' in annotation && !!annotation.intent?.endsWith('-dimension'),
  ).length;
  const started = useRef(false);

  // The same measurement the tool makes, with the page's scale and the tool's style.
  const measure = (kind: MeasurementKind) =>
    void measurement.createMeasurement({ kind, page: 0, points: SHAPES[kind] });

  // On load: an area, scrolled into view.
  useEffect(() => {
    if (!scale.ready || started.current) return;
    started.current = true;
    void measurement
      .createMeasurement({ kind: 'area', page: 0, points: SHAPES.area })
      .then(() => stage.reveal(0, { rect: LOWER_HALF }));
  }, [measurement, stage, scale.ready]);

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!scale.ready}
        onClick={() => measure('distance')}
      >
        Distance
      </button>
      <button
        type="button"
        className="button"
        disabled={!scale.ready}
        onClick={() => measure('perimeter')}
      >
        Perimeter
      </button>
      <button
        type="button"
        className="button"
        disabled={!scale.ready}
        onClick={() => measure('area')}
      >
        Area
      </button>
      <span className="spacer" />
      <output className="readout">
        {count} {count === 1 ? 'measurement' : 'measurements'} on the cover
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MeasureButtons />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`from-code.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 34px;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* A button that stays down while its tool or stamp is on */
.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--accent);
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Methods

Everything `useMeasurement()` gives you.

| Method                                                          | What it does                                                                                                                                                                      |
| :-------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listPresets()`                                                 | The scales to pick from, each with an `id` and a `label` such as `1:100`. [Set the scale](#set-the-scale)                                                                         |
| `setPreset(pages, presetId)`                                    | Gives pages a scale from the list. [Set the scale](#set-the-scale)                                                                                                                |
| `setScale(pages, scale)`                                        | Gives pages a scale of your own, in the form the [engine](https://www.embedpdf.com/docs/engine/annotations/measurements) reads and writes                                         |
| `clearScale(pages)`                                             | Puts pages back to the default scale                                                                                                                                              |
| `startCalibration()`                                            | Makes the next line the user draws the known length, then asks your UI for its real length. [Calibrate from a known length](#calibrate-from-a-known-length)                       |
| `calibrate(request, { applyTo? })`                              | Sets the scale from two points and the real length between them, on their page or on the pages in `applyTo`                                                                       |
| `dismissCalibration()`                                          | Cancels calibrating and drops the request                                                                                                                                         |
| `setUnit(pages, unit)`, `setAreaUnit(pages, unit)`              | Changes the unit lengths, or areas, are shown in. [Units and precision](#units-and-precision)                                                                                     |
| `setPrecision(pages, precision)`                                | Changes how finely a label rounds: `100` shows two decimals                                                                                                                       |
| `listUnits()`, `listAreaUnits()`                                | The length and area units to pick from, for a menu                                                                                                                                |
| `getPageScale(page)`                                            | A page's scale, whether it came from the file or is the default, and whether it has loaded. In a component, use `usePageScale(page)`.                                             |
| `ensureLoaded(page)`                                            | Loads a page's scale. Resolves at once when it's already known.                                                                                                                   |
| `getReadout(ref)`                                               | A measurement's `{ kind, value, label }`, or `{ unavailable }` with why there's none. In a component, use `useMeasurementReadout(ref)`. [Read a measurement](#read-a-measurement) |
| `measureDistance(page, from, to)`, `measureArea(page, corners)` | Measures in the page's scale without drawing anything: `{ value, label }`                                                                                                         |
| `createMeasurement({ kind, page, points })`                     | Makes the same measurement the tool would. Resolves `{ annotation }`. [Measure from code](#measure-from-code)                                                                     |
| `canCalibrate()`, `canMeasure(page)`                            | Whether setting a scale, and measuring on a page, are allowed. [Permissions](#permissions)                                                                                        |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()`   | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useMeasurementSettings()`. [Settings](#settings)                   |

`pages` is a page, a list of pages, or `'all'`. A scale change recalculates the measurements already
on those pages and resolves one report per page; pass `{ recalculate: false }` to keep their values.
Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

Everything `useMeasurementState()` gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.

| Field                | What it is                                                                                   | Read once                 |
| :------------------- | :------------------------------------------------------------------------------------------- | :------------------------ |
| `busy`               | Whether a scale change is still running                                                      | `isBusy()`                |
| `calibrationRequest` | The known length the user drew, `{ page, from, to }`, waiting for its real length, or `null` | `getCalibrationRequest()` |
| `lastReports`        | The reports of the last scale change, one per page                                           | `listLastReports()`       |

A page's scale and a measurement's readout come from `usePageScale(page)` and `useMeasurementReadout(ref)`.
Keeping them apart means a component re-renders only for its own page or measurement.

## Settings

Set them when you register the plugin, and change them while the app runs with
`measurement.updateSettings({ … })`; `measurement.resetSettings()` goes back to what you registered, and
`useMeasurementSettings()` gives you the current settings.

| Setting        | What it does                                                                        | Default                    |
| :------------- | :---------------------------------------------------------------------------------- | :------------------------- |
| `defaultScale` | The scale of a page that has none: `'metric'`, `'imperial'`, or a scale of your own | `'metric'` (1:1 in meters) |
| `presets`      | The scales `listPresets()` offers                                                   | the eight above            |

A measurement's look, such as its line, arrows and label position, is its tool's defaults; see
[Tools & defaults](tools).

## Events

**`events.tsx`**

```tsx
import { useState } from 'react';
import { useMeasurementEvent } from '@embedpdf/react/measurement';

export function ScaleNotice() {
  const [message, setMessage] = useState('');

  useMeasurementEvent(
    (measurement) => measurement.onScaleChanged,
    () => setMessage('The scale changed'),
  );

  return <p aria-live="polite">{message}</p>;
}
```

| Event                    | Fires when                                                  | Carries               |
| :----------------------- | :---------------------------------------------------------- | :-------------------- |
| `onScaleChanged`         | A page's scale changed                                      | `page`, `report`      |
| `onCalibrationRequested` | The user drew a known length, and your UI should ask for it | `request`             |
| `onCalibrationCompleted` | A page was calibrated                                       | `page`                |
| `onCalibrationDismissed` | Calibrating was cancelled                                   | —                     |
| `onSettingsChanged`      | The settings changed                                        | `settings`, `changed` |

`report` lists the measurements recalculated for the new scale, and any that couldn't be.

## Permissions

| Permission            | What it allows                         | Check                              |
| :-------------------- | :------------------------------------- | :--------------------------------- |
| `doc.annotate.modify` | Setting a page's scale and calibrating | `measurement.canCalibrate()`       |
| `annotations:create`  | Measuring                              | `measurement.canMeasure(page.ref)` |

`canMeasure` is also false while a page's scale is still loading.

## Good to know

### The scale is in the PDF

A page's scale is saved in the file, in the form Acrobat and Bluebeam use, so their measuring tools
read the same scale, and yours read theirs.

### Scales from map software

A few PDFs, such as maps, carry a geographic scale. It's kept as it is, and measurements on such a
page say `{ unavailable: 'foreign-measure' }`. See [Measurements](https://www.embedpdf.com/docs/engine/annotations/measurements)
for how scales and labels are stored.
