---
title: "Measurements — Svelte"
description: "Measure distances, perimeters and areas on drawings and plans at a real-world scale, and calibrate a page from a known length."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/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.svelte`**

```svelte
<script lang="ts">
  import { useInteraction } from '@embedpdf/svelte/interaction';

  const interaction = useInteraction();
</script>

<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:

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { measurementPlugin } from '@embedpdf/svelte/measurement';
  import { localEngine } from '@embedpdf/engine';
  import MeasureToolbar from './MeasureToolbar.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <MeasureToolbar />
    <Stage class="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);
}
```

**`MeasureToolbar.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { useInteraction, useInteractionState } from '@embedpdf/svelte/interaction';
  import { useMeasurement, usePageScale } from '@embedpdf/svelte/measurement';

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

  const interaction = useInteraction();
  const activeToolId = useInteractionState((state) => state.activeToolId);
  const measurement = useMeasurement();
  const scale = usePageScale(0); // the cover's scale

  // On load: the cover at 1:100, its width measured along the top, and the distance tool on.
  let started = false;
  $effect(() => {
    if (!scale.current.ready || started) return;
    started = true;
    untrack(() => {
      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'));
    });
  });
</script>

<div class="toolbar">
  <div class="segmented" role="group" aria-label="Tool">
    {#each TOOLS as tool (tool.id)}
      <button
        type="button"
        aria-pressed={activeToolId.current === tool.id}
        onclick={() => interaction.activateTool(tool.id)}
      >
        {tool.label}
      </button>
    {/each}
  </div>
  <span class="spacer"></span>
  <output class="badge">
    Scale {scale.current.measure?.subtype === 'rectilinear' ? scale.current.measure.ratio : '…'}
  </output>
</div>
```

## 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:

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { measurementPlugin } from '@embedpdf/svelte/measurement';
  import { localEngine } from '@embedpdf/engine';
  import MeasureCover from './MeasureCover.svelte';
  import ScaleToolbar from './ScaleToolbar.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <MeasureCover />
    <ScaleToolbar />
    <Stage class="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);
}
```

**`MeasureCover.svelte`**

```svelte
<!-- On load: a distance and an area on the cover, at 1:100, scrolled into view. -->
<script lang="ts">
  import { untrack } from 'svelte';
  import { useStage } from '@embedpdf/svelte/stage';
  import { useMeasurement, usePageScale } from '@embedpdf/svelte/measurement';

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

  const measurement = useMeasurement();
  const stage = useStage();
  const scale = usePageScale(0);

  let started = false;
  $effect(() => {
    if (!scale.current.ready || started) return;
    started = true;
    untrack(() => {
      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 })),
      );
    });
  });
</script>
```

**`ScaleToolbar.svelte`**

```svelte
<script lang="ts">
  import {
    useMeasurement,
    useMeasurementState,
    usePageScale,
    type AreaUnit,
    type LengthUnit,
  } from '@embedpdf/svelte/measurement';

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

  const measurement = useMeasurement();
  const busy = useMeasurementState((state) => state.busy);
  const scale = usePageScale(0);
  let preset = $state('metric-100');
  let unit = $state<LengthUnit>('m');
  let areaUnit = $state<AreaUnit>('m2');
  let precision = $state(100);

  // Every change recalculates the measurements already on the page.
  function choosePreset(event: Event & { currentTarget: HTMLSelectElement }) {
    preset = event.currentTarget.value;
    void measurement.setPreset(0, preset);
  }
  function chooseUnit(event: Event & { currentTarget: HTMLSelectElement }) {
    unit = event.currentTarget.value as LengthUnit;
    void measurement.setUnit(0, unit);
  }
  function chooseAreaUnit(event: Event & { currentTarget: HTMLSelectElement }) {
    areaUnit = event.currentTarget.value as AreaUnit;
    void measurement.setAreaUnit(0, areaUnit);
  }
  function choosePrecision(event: Event & { currentTarget: HTMLSelectElement }) {
    precision = Number(event.currentTarget.value);
    void measurement.setPrecision(0, precision);
  }
</script>

<div class="toolbar">
  <label class="label">
    Scale
    <select class="field" value={preset} disabled={busy.current} onchange={choosePreset}>
      {#each measurement.listPresets() as choice (choice.id)}
        <option value={choice.id}>{choice.label}</option>
      {/each}
    </select>
  </label>
  <label class="label">
    Length
    <select class="field" value={unit} disabled={busy.current} onchange={chooseUnit}>
      {#each measurement.listUnits() as choice (choice)}
        <option value={choice}>{choice}</option>
      {/each}
    </select>
  </label>
  <label class="label">
    Area
    <select class="field" value={areaUnit} disabled={busy.current} onchange={chooseAreaUnit}>
      {#each measurement.listAreaUnits() as choice (choice)}
        <option value={choice}>{choice}</option>
      {/each}
    </select>
  </label>
  <label class="label">
    Steps
    <select class="field" value={precision} disabled={busy.current} onchange={choosePrecision}>
      {#each PRECISIONS as choice (choice)}
        <option value={choice}>{choice === 1 ? 'Whole' : `1/${choice}`}</option>
      {/each}
    </select>
  </label>
  <span class="spacer"></span>
  <output class="badge">
    {scale.current.measure?.subtype === 'rectilinear' ? scale.current.measure.ratio : '…'}
  </output>
</div>
```

## 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.svelte`**

```svelte
<script lang="ts">
  import { useMeasurement, useMeasurementState } from '@embedpdf/svelte/measurement';

  import LengthPrompt from './LengthPrompt.svelte';

  const measurement = useMeasurement();
  const state = useMeasurementState();

  function apply(value: number) {
    const request = state.calibrationRequest;
    if (request) measurement.calibrate({ ...request, distance: { value, unit: 'cm' } });
  }
</script>

{#if state.calibrationRequest}
  <LengthPrompt onSubmit={apply} onCancel={() => measurement.dismissCalibration()} />
{/if}
```

`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.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { measurementPlugin } from '@embedpdf/svelte/measurement';
  import { localEngine } from '@embedpdf/engine';
  import Calibration from './Calibration.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Calibration />
    <Stage class="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);
}
```

**`Calibration.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { useInteraction } from '@embedpdf/svelte/interaction';
  import {
    useMeasurement,
    useMeasurementState,
    usePageScale,
    type LengthUnit,
  } from '@embedpdf/svelte/measurement';

  const measurement = useMeasurement();
  const interaction = useInteraction();
  const measurementState = useMeasurementState();
  const scale = usePageScale(0);
  let length = $state(20);
  let unit = $state<LengthUnit>('cm');

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

  // The plugin asks for the real length of the line you drew.
  function setScale(event: SubmitEvent) {
    event.preventDefault();
    const request = measurementState.calibrationRequest;
    if (!request) return;
    void measurement
      .calibrate({ ...request, distance: { value: length, unit } })
      .then(() => interaction.activateTool('distance'));
  }
</script>

{#if measurementState.calibrationRequest}
  <form class="toolbar" onsubmit={setScale}>
    <span class="readout">That line is</span>
    <input
      class="field length"
      type="number"
      min="0"
      step="any"
      aria-label="Its real length"
      bind:value={length}
    />
    <select class="field" aria-label="Unit" bind:value={unit}>
      {#each measurement.listUnits() as choice (choice)}
        <option value={choice}>{choice}</option>
      {/each}
    </select>
    <button type="submit" class="button" disabled={measurementState.busy || !(length > 0)}>
      Set the scale
    </button>
    <button type="button" class="button" onclick={() => measurement.dismissCalibration()}>
      Cancel
    </button>
  </form>
{:else}
  <div class="toolbar">
    <button
      type="button"
      class="button"
      disabled={!measurement.canCalibrate()}
      onclick={() => measurement.startCalibration()}
    >
      Calibrate
    </button>
    <output class="readout">
      Drag along something you know the length of, then measure with the new scale
    </output>
    <span class="spacer"></span>
    <output class="badge">
      {scale.current.measure?.subtype === 'rectilinear' ? scale.current.measure.ratio : '…'}
    </output>
  </div>
{/if}
```

## 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.svelte`**

```svelte
<script lang="ts">
  import type { AnnotationRef } from '@embedpdf/svelte/annotation';
  import { useMeasurementReadout } from '@embedpdf/svelte/measurement';

  let { annotation }: { annotation: AnnotationRef } = $props();

  const readout = useMeasurementReadout(() => annotation);
  // readout.current: { kind: 'distance', value: 3.42, label: '3.42 m' }
</script>

<span>{'unavailable' in readout.current ? '—' : readout.current.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' }`.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { measurementPlugin } from '@embedpdf/svelte/measurement';
  import { localEngine } from '@embedpdf/engine';
  import MeasureCover from './MeasureCover.svelte';
  import Readouts from './Readouts.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <MeasureCover />
    <div class="viewer">
      <Stage class="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);
}
```

**`MeasureCover.svelte`**

```svelte
<!-- On load: the cover at 1:50, and one measurement of each kind on it, scrolled into view. -->
<script lang="ts">
  import { untrack } from 'svelte';
  import { useStage } from '@embedpdf/svelte/stage';
  import { useMeasurement, usePageScale } from '@embedpdf/svelte/measurement';

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

  const measurement = useMeasurement();
  const stage = useStage();
  const scale = usePageScale(0);

  let started = false;
  $effect(() => {
    if (!scale.current.ready || started) return;
    started = true;
    untrack(() => {
      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 })),
      );
    });
  });
</script>
```

**`Readout.svelte`**

```svelte
<!-- One measurement's readout: a click selects it on the page. -->
<script lang="ts">
  import { useAnnotation, type Annotation } from '@embedpdf/svelte/annotation';
  import { useMeasurementReadout } from '@embedpdf/svelte/measurement';

  let { annotation }: { annotation: Annotation } = $props();

  const readout = useMeasurementReadout(() => annotation.ref);
  const annotations = useAnnotation();
</script>

<li>
  <button type="button" class="row" onclick={() => annotations.selection.set([annotation.ref])}>
    <span class="kind">{'unavailable' in readout.current ? 'No scale' : readout.current.kind}</span>
    <span class="value">{'unavailable' in readout.current ? '—' : readout.current.label}</span>
  </button>
</li>
```

**`Readouts.svelte`**

```svelte
<script lang="ts">
  import { annotationKey, useAnnotationList, type Annotation } from '@embedpdf/svelte/annotation';
  import Readout from './Readout.svelte';

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

  const cover = useAnnotationList({ pages: [0] });
  const measurements = $derived(cover.current.filter(isMeasurement));
</script>

<ul class="readouts" aria-label="Measurements on the cover">
  {#each measurements as annotation (annotationKey(annotation.ref))}
    <Readout {annotation} />
  {/each}
</ul>
```

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.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { measurementPlugin } from '@embedpdf/svelte/measurement';
  import { localEngine } from '@embedpdf/engine';
  import MeasureButtons from './MeasureButtons.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <MeasureButtons />
    <Stage class="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);
}
```

**`MeasureButtons.svelte`**

```svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { useStage } from '@embedpdf/svelte/stage';
  import { useAnnotationList } from '@embedpdf/svelte/annotation';
  import {
    useMeasurement,
    usePageScale,
    type MeasurementKind,
  } from '@embedpdf/svelte/measurement';

  // 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 };

  const measurement = useMeasurement();
  const stage = useStage();
  const scale = usePageScale(0);
  const cover = useAnnotationList({ pages: [0] });
  const count = $derived(
    cover.current.filter(
      (annotation) => 'intent' in annotation && !!annotation.intent?.endsWith('-dimension'),
    ).length,
  );

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

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

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

## 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, as one reactive object: read its fields, and your markup updates when they change. 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)`.

## 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.svelte`**

```svelte
<script lang="ts">
  import { useMeasurementEvent } from '@embedpdf/svelte/measurement';

  let message = $state('');

  useMeasurementEvent(
    (measurement) => measurement.onScaleChanged,
    () => (message = 'The scale changed'),
  );
</script>

<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.
