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

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

const interaction = useInteraction();
</script>

<template>
  <button @click="interaction.activateTool('distance')">Measure</button>
</template>
```

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

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MeasureToolbar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

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

```vue
<script setup lang="ts">
import { watch } from 'vue';
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';
import { useMeasurement, usePageScale } from '@embedpdf/vue/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();
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;
watch(
  () => scale.value.ready,
  (ready) => {
    if (!ready || started) return;
    started = 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'));
  },
  { immediate: true },
);
</script>

<template>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Tool">
      <button
        v-for="tool in TOOLS"
        :key="tool.id"
        type="button"
        :aria-pressed="activeToolId === tool.id"
        @click="interaction.activateTool(tool.id)"
      >
        {{ tool.label }}
      </button>
    </div>
    <span class="spacer" />
    <output class="badge">
      Scale {{ scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…' }}
    </output>
  </div>
</template>
```

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

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MeasureCover />
      <ScaleToolbar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

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

```vue
<!-- On load: a distance and an area on the cover, at 1:100, scrolled into view. -->
<script setup lang="ts">
import { watch } from 'vue';
import { useStage } from '@embedpdf/vue/stage';
import { useMeasurement, usePageScale } from '@embedpdf/vue/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;
watch(
  () => scale.value.ready,
  (ready) => {
    if (!ready || started) return;
    started = 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 })),
    );
  },
  { immediate: true },
);
</script>

<template></template>
```

**`ScaleToolbar.vue`**

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

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

const measurement = useMeasurement();
const { busy } = useMeasurementState();
const scale = usePageScale(0);
const preset = ref('metric-100');
const unit = ref<LengthUnit>('m');
const areaUnit = ref<AreaUnit>('m2');
const precision = ref(100);

// Every change recalculates the measurements already on the page.
watch(preset, (id) => void measurement.setPreset(0, id));
watch(unit, (next) => void measurement.setUnit(0, next));
watch(areaUnit, (next) => void measurement.setAreaUnit(0, next));
watch(precision, (next) => void measurement.setPrecision(0, next));
</script>

<template>
  <div class="toolbar">
    <label class="label">
      Scale
      <select v-model="preset" class="field" :disabled="busy">
        <option v-for="choice in measurement.listPresets()" :key="choice.id" :value="choice.id">
          {{ choice.label }}
        </option>
      </select>
    </label>
    <label class="label">
      Length
      <select v-model="unit" class="field" :disabled="busy">
        <option v-for="choice in measurement.listUnits()" :key="choice" :value="choice">
          {{ choice }}
        </option>
      </select>
    </label>
    <label class="label">
      Area
      <select v-model="areaUnit" class="field" :disabled="busy">
        <option v-for="choice in measurement.listAreaUnits()" :key="choice" :value="choice">
          {{ choice }}
        </option>
      </select>
    </label>
    <label class="label">
      Steps
      <select v-model="precision" class="field" :disabled="busy">
        <option v-for="choice in PRECISIONS" :key="choice" :value="choice">
          {{ choice === 1 ? 'Whole' : `1/${choice}` }}
        </option>
      </select>
    </label>
    <span class="spacer" />
    <output class="badge">
      {{ scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…' }}
    </output>
  </div>
</template>
```

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

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

import LengthPrompt from './LengthPrompt.vue';

const measurement = useMeasurement();
const { calibrationRequest } = useMeasurementState();

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

<template>
  <LengthPrompt
    v-if="calibrationRequest"
    @submit="apply"
    @cancel="measurement.dismissCalibration()"
  />
</template>
```

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

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <Calibration />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

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

```vue
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useInteraction } from '@embedpdf/vue/interaction';
import { useMeasurement, useMeasurementState, usePageScale } from '@embedpdf/vue/measurement';
import type { LengthUnit } from '@embedpdf/vue/measurement';

const measurement = useMeasurement();
const interaction = useInteraction();
const { calibrationRequest, busy } = useMeasurementState();
const scale = usePageScale(0);
const length = ref('20');
const unit = ref<LengthUnit>('cm');
const value = computed(() => Number(length.value));

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

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

<template>
  <form v-if="calibrationRequest" class="toolbar" @submit.prevent="setScale">
    <span class="readout">That line is</span>
    <input
      v-model="length"
      class="field length"
      type="number"
      min="0"
      step="any"
      aria-label="Its real length"
    />
    <select v-model="unit" class="field" aria-label="Unit">
      <option v-for="choice in measurement.listUnits()" :key="choice" :value="choice">
        {{ choice }}
      </option>
    </select>
    <button type="submit" class="button" :disabled="busy || !(value > 0)">Set the scale</button>
    <button type="button" class="button" @click="measurement.dismissCalibration()">Cancel</button>
  </form>
  <div v-else class="toolbar">
    <button
      type="button"
      class="button"
      :disabled="!measurement.canCalibrate()"
      @click="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" />
    <output class="badge">
      {{ scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…' }}
    </output>
  </div>
</template>
```

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

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

const { annotation } = defineProps<{ annotation: AnnotationRef }>();

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

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

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

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MeasureCover />
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <Readouts />
      </div>
    </DocumentGate>
  </Viewer>
</template>
```

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

```vue
<!-- On load: the cover at 1:50, and one measurement of each kind on it, scrolled into view. -->
<script setup lang="ts">
import { watch } from 'vue';
import { useStage } from '@embedpdf/vue/stage';
import { useMeasurement, usePageScale } from '@embedpdf/vue/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;
watch(
  () => scale.value.ready,
  (ready) => {
    if (!ready || started) return;
    started = 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 })),
    );
  },
  { immediate: true },
);
</script>

<template></template>
```

**`Readout.vue`**

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

const { annotation } = defineProps<{ annotation: Annotation }>();

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

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

**`Readouts.vue`**

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

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

const onCover = useAnnotationList({ pages: [0] });
const measurements = computed(() => onCover.value.filter(isMeasurement));
</script>

<template>
  <ul class="readouts" aria-label="Measurements on the cover">
    <Readout
      v-for="annotation in measurements"
      :key="annotationKey(annotation.ref)"
      :annotation="annotation"
    />
  </ul>
</template>
```

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

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

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <MeasureButtons />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

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

```vue
<script setup lang="ts">
import { computed, watch } from 'vue';
import { useStage } from '@embedpdf/vue/stage';
import { useAnnotationList } from '@embedpdf/vue/annotation';
import { useMeasurement, usePageScale } from '@embedpdf/vue/measurement';
import type { MeasurementKind } from '@embedpdf/vue/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 onCover = useAnnotationList({ pages: [0] });
const count = computed(
  () =>
    onCover.value.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;
watch(
  () => scale.value.ready,
  (ready) => {
    if (!ready || started) return;
    started = true;
    void measurement
      .createMeasurement({ kind: 'area', page: 0, points: SHAPES.area })
      .then(() => stage.reveal(0, { rect: LOWER_HALF }));
  },
  { immediate: true },
);
</script>

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

## 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 refs: your template updates 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)`.

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

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

const message = ref('');

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

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

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