---
title: "Measurements — Angular"
description: "Measure distances, perimeters and areas on drawings and plans at a real-world scale, and calibrate a page from a known length."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/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 `withMeasurement()` next to
`withInteraction()` and `withAnnotation()`, and turn on a tool:

**`tool.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';

@Component({
  selector: 'app-measure-button',
  template: `<button (click)="interaction.activateTool('distance')">Measure</button>`,
})
export class MeasureButton {
  protected readonly interaction = inject(EpdfInteraction);
}
```

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

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

**`basic.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfMeasurement, withMeasurement } from '@embedpdf/angular/measurement';
import { localEngine } from '@embedpdf/engine';

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

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

@Component({
  selector: 'demo-measure-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <div class="segmented" role="group" aria-label="Tool">
        @for (tool of tools; track tool.id) {
          <button
            type="button"
            [attr.aria-pressed]="interaction.activeToolId() === tool.id"
            (click)="interaction.activateTool(tool.id)"
          >
            {{ tool.label }}
          </button>
        }
      </div>
      <span class="spacer"></span>
      <output class="badge">Scale {{ ratio() }}</output>
    </div>
  `,
})
export class MeasureToolbar {
  protected readonly interaction = inject(EpdfInteraction);
  private readonly measurement = inject(EpdfMeasurement);
  protected readonly tools = TOOLS;
  private readonly scale = this.measurement.scaleOf(0); // the cover's scale
  protected readonly ratio = computed(() => {
    const measure = this.scale().measure;
    return measure?.subtype === 'rectilinear' ? measure.ratio : '…';
  });
  private started = false;

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

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    MeasureToolbar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withMeasurement(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './basic.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-measure-toolbar />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`basic.css`**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Set the scale

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

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

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

**`scale.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  untracked,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import {
  EpdfMeasurement,
  withMeasurement,
  type AreaUnit,
  type LengthUnit,
} from '@embedpdf/angular/measurement';
import { localEngine } from '@embedpdf/engine';

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

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

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

@Component({
  selector: 'demo-scale-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <label class="label">
        Scale
        <select
          #presetField
          class="field"
          [disabled]="measurement.busy()"
          (change)="setPreset(presetField.value)"
        >
          @for (choice of presets; track choice.id) {
            <option [value]="choice.id" [selected]="choice.id === preset()">
              {{ choice.label }}
            </option>
          }
        </select>
      </label>
      <label class="label">
        Length
        <select
          #unitField
          class="field"
          [disabled]="measurement.busy()"
          (change)="setUnit(unitField.value)"
        >
          @for (choice of units; track choice) {
            <option [value]="choice" [selected]="choice === unit()">{{ choice }}</option>
          }
        </select>
      </label>
      <label class="label">
        Area
        <select
          #areaUnitField
          class="field"
          [disabled]="measurement.busy()"
          (change)="setAreaUnit(areaUnitField.value)"
        >
          @for (choice of areaUnits; track choice) {
            <option [value]="choice" [selected]="choice === areaUnit()">{{ choice }}</option>
          }
        </select>
      </label>
      <label class="label">
        Steps
        <select
          #precisionField
          class="field"
          [disabled]="measurement.busy()"
          (change)="setPrecision(+precisionField.value)"
        >
          @for (choice of precisions; track choice) {
            <option [value]="choice" [selected]="choice === precision()">
              {{ choice === 1 ? 'Whole' : '1/' + choice }}
            </option>
          }
        </select>
      </label>
      <span class="spacer"></span>
      <output class="badge">{{ ratio() }}</output>
    </div>
  `,
})
export class ScaleToolbar {
  protected readonly measurement = inject(EpdfMeasurement);
  protected readonly presets = this.measurement.listPresets();
  protected readonly units = this.measurement.listUnits();
  protected readonly areaUnits = this.measurement.listAreaUnits();
  protected readonly precisions = PRECISIONS;
  protected readonly preset = signal('metric-100');
  protected readonly unit = signal<LengthUnit>('m');
  protected readonly areaUnit = signal<AreaUnit>('m2');
  protected readonly precision = signal(100);
  private readonly scale = this.measurement.scaleOf(0);
  protected readonly ratio = computed(() => {
    const measure = this.scale().measure;
    return measure?.subtype === 'rectilinear' ? measure.ratio : '…';
  });

  // Every change recalculates the measurements already on the page.
  protected setPreset(preset: string) {
    this.preset.set(preset);
    void this.measurement.setPreset(0, preset);
  }

  // A <select> gives a string: one of the units the plugin listed.
  protected setUnit(value: string) {
    const unit = value as LengthUnit;
    this.unit.set(unit);
    void this.measurement.setUnit(0, unit);
  }

  protected setAreaUnit(value: string) {
    const unit = value as AreaUnit;
    this.areaUnit.set(unit);
    void this.measurement.setAreaUnit(0, unit);
  }

  protected setPrecision(precision: number) {
    this.precision.set(precision);
    void this.measurement.setPrecision(0, precision);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    ScaleToolbar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withMeasurement(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './scale.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-scale-toolbar />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly measurement = inject(EpdfMeasurement);
  private readonly coverScale = this.measurement.scaleOf(0);
  private readonly stage = viewChild(EpdfStage);
  private started = false;

  constructor() {
    // On load: a distance and an area on the cover, at 1:100, scrolled into view.
    effect(() => {
      const stage = this.stage();
      if (!stage || !this.coverScale().ready || this.started) return;
      this.started = true;
      untracked(() => this.measureCover(stage));
    });
  }

  private measureCover(stage: EpdfStage) {
    void this.measurement.setPreset(0, 'metric-100').then(() =>
      Promise.all([
        this.measurement.createMeasurement({
          kind: 'distance',
          page: 0,
          points: [
            { x: 60, y: 740 },
            { x: 550, y: 740 },
          ],
        }),
        this.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 })),
    );
  }
}
```

**`scale.css`**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Calibrate from a known length

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

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

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

**`calibration.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfMeasurement, type CalibrationRequest } from '@embedpdf/angular/measurement';

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

@Component({
  selector: 'app-calibration-dialog',
  imports: [LengthPrompt],
  template: `
    @if (measurement.calibrationRequest(); as request) {
      <app-length-prompt
        (submitted)="apply(request, $event)"
        (cancelled)="measurement.dismissCalibration()"
      />
    }
  `,
})
export class CalibrationDialog {
  protected readonly measurement = inject(EpdfMeasurement);

  protected apply(request: CalibrationRequest, value: number) {
    void this.measurement.calibrate({ ...request, distance: { value, unit: 'cm' } });
  }
}
```

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

**`calibrate.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import {
  EpdfMeasurement,
  withMeasurement,
  type CalibrationRequest,
  type LengthUnit,
} from '@embedpdf/angular/measurement';
import { localEngine } from '@embedpdf/engine';

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

@Component({
  selector: 'demo-calibration',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <!-- The plugin asks for the real length of the line you drew. -->
    @if (measurement.calibrationRequest(); as request) {
      <form class="toolbar" (submit)="$event.preventDefault(); calibrate(request)">
        <span class="readout">That line is</span>
        <input
          #lengthField
          class="field length"
          type="number"
          min="0"
          step="any"
          aria-label="Its real length"
          [value]="length()"
          (input)="length.set(lengthField.value)"
        />
        <select #unitField class="field" aria-label="Unit" (change)="setUnit(unitField.value)">
          @for (choice of units; track choice) {
            <option [value]="choice" [selected]="choice === unit()">{{ choice }}</option>
          }
        </select>
        <button type="submit" class="button" [disabled]="measurement.busy() || !(value() > 0)">
          Set the scale
        </button>
        <button type="button" class="button" (click)="measurement.dismissCalibration()">
          Cancel
        </button>
      </form>
    } @else {
      <div 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"></span>
        <output class="badge">{{ ratio() }}</output>
      </div>
    }
  `,
})
export class Calibration {
  protected readonly measurement = inject(EpdfMeasurement);
  private readonly interaction = inject(EpdfInteraction);
  protected readonly units = this.measurement.listUnits();
  protected readonly length = signal('20');
  protected readonly value = computed(() => Number(this.length()));
  protected readonly unit = signal<LengthUnit>('cm');
  private readonly scale = this.measurement.scaleOf(0);
  protected readonly ratio = computed(() => {
    const measure = this.scale().measure;
    return measure?.subtype === 'rectilinear' ? measure.ratio : '…';
  });
  private started = false;

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

  // A <select> gives a string: one of the units the plugin listed.
  protected setUnit(value: string) {
    this.unit.set(value as LengthUnit);
  }

  protected calibrate(request: CalibrationRequest) {
    void this.measurement
      .calibrate({ ...request, distance: { value: this.value(), unit: this.unit() } })
      .then(() => this.interaction.activateTool('distance'));
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    Calibration,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withMeasurement(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './calibrate.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-calibration />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`calibrate.css`**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Units and precision

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

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

## Read a measurement

**`readout.ts`**

```typescript
import { Component, computed, inject, input } from '@angular/core';
import type { AnnotationRef } from '@embedpdf/angular/annotation';
import { EpdfMeasurement } from '@embedpdf/angular/measurement';

@Component({
  selector: 'app-measurement-label',
  template: `<span>{{ label() }}</span>`,
})
export class MeasurementLabel {
  readonly annotation = input.required<AnnotationRef>();

  private readonly readout = inject(EpdfMeasurement).readoutOf(this.annotation);
  // { kind: 'distance', value: 3.42, label: '3.42 m' }

  protected readonly label = computed(() => {
    const readout = this.readout();
    return 'unavailable' in readout ? '—' : readout.label;
  });
}
```

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

**`readout.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  untracked,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import {
  annotationKey,
  EpdfAnnotation,
  EpdfAnnotationLayer,
  withAnnotation,
  type Annotation,
} from '@embedpdf/angular/annotation';
import { EpdfMeasurement, withMeasurement } from '@embedpdf/angular/measurement';
import { localEngine } from '@embedpdf/engine';

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

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

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

@Component({
  selector: 'li[demoReadout]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button type="button" class="row" (click)="select()">
      <span class="kind">{{ kind() }}</span>
      <span class="value">{{ value() }}</span>
    </button>
  `,
})
export class Readout {
  readonly annotation = input.required<Annotation>();

  private readonly selection = inject(EpdfAnnotation).selection;
  private readonly readout = inject(EpdfMeasurement).readoutOf(() => this.annotation().ref);
  protected readonly kind = computed(() => {
    const readout = this.readout();
    return 'unavailable' in readout ? 'No scale' : readout.kind;
  });
  protected readonly value = computed(() => {
    const readout = this.readout();
    return 'unavailable' in readout ? '—' : readout.label;
  });

  protected select() {
    this.selection.set([this.annotation().ref]);
  }
}

@Component({
  selector: 'ul[demoReadouts]',
  imports: [Readout],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'readouts', 'aria-label': 'Measurements on the cover' },
  template: `
    @for (annotation of measurements(); track key(annotation.ref)) {
      <li demoReadout [annotation]="annotation"></li>
    }
  `,
})
export class Readouts {
  private readonly onCover = inject(EpdfAnnotation).watch({ pages: [0] });
  protected readonly measurements = computed(() => this.onCover().filter(isMeasurement));
  protected readonly key = annotationKey;
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    Readouts,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withMeasurement(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './readout.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="viewer">
        <epdf-stage class="stage">
          <ng-template epdfPage>
            <epdf-render-layer />
            <epdf-annotation-layer />
          </ng-template>
        </epdf-stage>
        <ul demoReadouts></ul>
      </div>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly measurement = inject(EpdfMeasurement);
  private readonly coverScale = this.measurement.scaleOf(0);
  private readonly stage = viewChild(EpdfStage);
  private started = false;

  constructor() {
    // On load: the cover at 1:50, and one measurement of each kind on it, scrolled into view.
    effect(() => {
      const stage = this.stage();
      if (!stage || !this.coverScale().ready || this.started) return;
      this.started = true;
      untracked(() => this.measureCover(stage));
    });
  }

  private measureCover(stage: EpdfStage) {
    void this.measurement.setPreset(0, 'metric-50').then(() =>
      Promise.all([
        this.measurement.createMeasurement({
          kind: 'distance',
          page: 0,
          points: [
            { x: 60, y: 740 },
            { x: 550, y: 740 },
          ],
        }),
        this.measurement.createMeasurement({
          kind: 'perimeter',
          page: 0,
          points: [
            { x: 60, y: 580 },
            { x: 160, y: 640 },
            { x: 260, y: 590 },
          ],
        }),
        this.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 })),
    );
  }
}
```

**`readout.css`**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Measure from code

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

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

**`from-code.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import {
  EpdfMeasurement,
  withMeasurement,
  type MeasurementKind,
} from '@embedpdf/angular/measurement';
import { localEngine } from '@embedpdf/engine';

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

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

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

/** Next to `<epdf-stage #stage="epdfStage">`: `<demo-measure-buttons [stage]="stage" />`. */
@Component({
  selector: 'demo-measure-buttons',
  changeDetection: ChangeDetectionStrategy.OnPush,
  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"></span>
      <output class="readout">
        {{ count() }} {{ count() === 1 ? 'measurement' : 'measurements' }} on the cover
      </output>
    </div>
  `,
})
export class MeasureButtons {
  readonly stage = input.required<EpdfStage>();

  private readonly measurement = inject(EpdfMeasurement);
  protected readonly scale = this.measurement.scaleOf(0);
  private readonly onCover = inject(EpdfAnnotation).watch({ pages: [0] });
  protected readonly count = computed(
    () =>
      this.onCover().filter(
        (annotation) => 'intent' in annotation && !!annotation.intent?.endsWith('-dimension'),
      ).length,
  );
  private started = false;

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

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

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    MeasureButtons,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withMeasurement(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './from-code.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-measure-buttons [stage]="stage" />
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`from-code.css`**

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

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

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

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

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

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

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

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

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

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

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

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

## Methods

Everything `inject(EpdfMeasurement)` 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 `measurement.scaleOf(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 `measurement.readoutOf(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 `inject(EpdfMeasurement).settings()`. [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

Every value is a signal on `inject(EpdfMeasurement)`: read it in a template or a `computed()`, and your view updates when it 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 `measurement.scaleOf(page)` and `measurement.readoutOf(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
`inject(EpdfMeasurement).settings()` 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.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { EpdfMeasurement } from '@embedpdf/angular/measurement';

@Component({
  selector: 'app-scale-notice',
  template: `<p aria-live="polite">{{ message() }}</p>`,
})
export class ScaleNotice {
  private readonly measurement = inject(EpdfMeasurement);

  protected readonly message = toSignal(
    this.measurement.scaleChanged$.pipe(map(() => 'The scale changed')),
    { initialValue: '' },
  );
}
```

| Event                   | Fires when                                                  | Carries               |
| :---------------------- | :---------------------------------------------------------- | :-------------------- |
| `scaleChanged$`         | A page's scale changed                                      | `page`, `report`      |
| `calibrationRequested$` | The user drew a known length, and your UI should ask for it | `request`             |
| `calibrationCompleted$` | A page was calibrated                                       | `page`                |
| `calibrationDismissed$` | Calibrating was cancelled                                   | —                     |
| `settingsChanged$`      | 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.
