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:
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:
Loading live preview…
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 {}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:
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 pageMeasurements already on the page are recalculated for the new scale. Change the scale, the units or the precision, and the two measurements follow:
Loading live preview…
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 })),
);
}
}Calibrate from a known length#
When the scale isn’t printed, measure something you know, such as a door that’s 90 centimeters wide:
measurement.startCalibration(); // the next line the user draws is the known lengthThe plugin asks your UI for the real length, through the measurement.calibrationRequest() signal:
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.
Loading live preview…
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 {}Units and precision#
await measurement.setUnit([page.ref], 'ft');
await measurement.setAreaUnit([page.ref], 'ft2'); // or 'm2', 'ha', 'acre'
await measurement.setPrecision([page.ref], 100); // two decimalslistUnits() 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#
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' }.
Loading live preview…
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 })),
);
}
}To measure without drawing anything, for a tooltip that follows the pointer:
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#
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.
Loading live preview…
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 {}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 |
setPreset(pages, presetId) | Gives pages a scale from the list. Set the scale |
setScale(pages, scale) | Gives pages a scale of your own, in the form the engine 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(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 |
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 |
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 |
canCalibrate(), canMeasure(page) | Whether setting a scale, and measuring on a page, are allowed. 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 |
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.
Events#
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
for how scales and labels are stored.
Your feedback goes directly to the documentation team.