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:
<script lang="ts">
import { useInteraction } from '@embedpdf/svelte/interaction';
const interaction = useInteraction();
</script>
<button onclick={() => interaction.activateTool('distance')}>Measure</button>| Tool | Measures | How |
|---|---|---|
distance | The length of a line | Drag from one point to the other |
perimeter | The length of a path of several lines | Click each point, double-click to end |
area | The area of a shape | Click each corner, double-click to end |
Each measurement is an annotation with its value as a label, so it shows the same in Acrobat and other PDF apps, and you can select, move and restyle it like any annotation. The example measures the cover’s width at 1:100; pick a tool and measure something else:
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { measurementPlugin } from '@embedpdf/svelte/measurement';
import { localEngine } from '@embedpdf/engine';
import MeasureToolbar from './MeasureToolbar.svelte';
import '../basic.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
measurementPlugin(),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<MeasureToolbar />
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
</Stage>
</DocumentGate>
</Viewer>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…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { measurementPlugin } from '@embedpdf/svelte/measurement';
import { localEngine } from '@embedpdf/engine';
import MeasureCover from './MeasureCover.svelte';
import ScaleToolbar from './ScaleToolbar.svelte';
import '../scale.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
measurementPlugin(),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<MeasureCover />
<ScaleToolbar />
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
</Stage>
</DocumentGate>
</Viewer>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 useMeasurementState().calibrationRequest:
<script lang="ts">
import { useMeasurement, useMeasurementState } from '@embedpdf/svelte/measurement';
import LengthPrompt from './LengthPrompt.svelte';
const measurement = useMeasurement();
const state = useMeasurementState();
function apply(value: number) {
const request = state.calibrationRequest;
if (request) measurement.calibrate({ ...request, distance: { value, unit: 'cm' } });
}
</script>
{#if state.calibrationRequest}
<LengthPrompt onSubmit={apply} onCancel={() => measurement.dismissCalibration()} />
{/if}calibrate() sets the page’s scale from the two points and the length you give. Pass
{ applyTo: 'all' } to use the same scale on every page.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { measurementPlugin } from '@embedpdf/svelte/measurement';
import { localEngine } from '@embedpdf/engine';
import Calibration from './Calibration.svelte';
import '../calibrate.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
measurementPlugin(),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<Calibration />
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
</Stage>
</DocumentGate>
</Viewer>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#
<script lang="ts">
import type { AnnotationRef } from '@embedpdf/svelte/annotation';
import { useMeasurementReadout } from '@embedpdf/svelte/measurement';
let { annotation }: { annotation: AnnotationRef } = $props();
const readout = useMeasurementReadout(() => annotation);
// readout.current: { kind: 'distance', value: 3.42, label: '3.42 m' }
</script>
<span>{'unavailable' in readout.current ? '—' : readout.current.label}</span>label is the text shown on the page, in the page’s units. Without a scale to compute from, a
readout says why: { unavailable: 'no-scale' }.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { measurementPlugin } from '@embedpdf/svelte/measurement';
import { localEngine } from '@embedpdf/engine';
import MeasureCover from './MeasureCover.svelte';
import Readouts from './Readouts.svelte';
import '../readout.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
measurementPlugin(),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<MeasureCover />
<div class="viewer">
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
</Stage>
<Readouts />
</div>
</DocumentGate>
</Viewer>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);usePageScale(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…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { measurementPlugin } from '@embedpdf/svelte/measurement';
import { localEngine } from '@embedpdf/engine';
import MeasureButtons from './MeasureButtons.svelte';
import '../from-code.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
measurementPlugin(),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<MeasureButtons />
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
</Stage>
</DocumentGate>
</Viewer>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 |
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 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 |
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 useMeasurementSettings(). Settings |
pages is a page, a list of pages, or 'all'. A scale change recalculates the measurements already
on those pages and resolves one report per page; pass { recalculate: false } to keep their values.
Every method that returns a promise also takes a signal in its options, to cancel it.
State#
Everything useMeasurementState() gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
busy | Whether a scale change is still running | isBusy() |
calibrationRequest | The known length the user drew, { page, from, to }, waiting for its real length, or null | getCalibrationRequest() |
lastReports | The reports of the last scale change, one per page | listLastReports() |
A page’s scale and a measurement’s readout come from usePageScale(page) and useMeasurementReadout(ref).
Settings#
Set them when you register the plugin, and change them while the app runs with
measurement.updateSettings({ … }); measurement.resetSettings() goes back to what you registered, and
useMeasurementSettings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
defaultScale | The scale of a page that has none: 'metric', 'imperial', or a scale of your own | 'metric' (1:1 in meters) |
presets | The scales listPresets() offers | the eight above |
A measurement’s look, such as its line, arrows and label position, is its tool’s defaults; see Tools & defaults.
Events#
<script lang="ts">
import { useMeasurementEvent } from '@embedpdf/svelte/measurement';
let message = $state('');
useMeasurementEvent(
(measurement) => measurement.onScaleChanged,
() => (message = 'The scale changed'),
);
</script>
<p aria-live="polite">{message}</p>| Event | Fires when | Carries |
|---|---|---|
onScaleChanged | A page’s scale changed | page, report |
onCalibrationRequested | The user drew a known length, and your UI should ask for it | request |
onCalibrationCompleted | A page was calibrated | page |
onCalibrationDismissed | Calibrating was cancelled | — |
onSettingsChanged | The settings changed | settings, changed |
report lists the measurements recalculated for the new scale, and any that couldn’t be.
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.annotate.modify | Setting a page’s scale and calibrating | measurement.canCalibrate() |
annotations:create | Measuring | measurement.canMeasure(page.ref) |
canMeasure is also false while a page’s scale is still loading.
Good to know#
The scale is in the PDF#
A page’s scale is saved in the file, in the form Acrobat and Bluebeam use, so their measuring tools read the same scale, and yours read theirs.
Scales from map software#
A few PDFs, such as maps, carry a geographic scale. It’s kept as it is, and measurements on such a
page say { unavailable: 'foreign-measure' }. See Measurements
for how scales and labels are stored.
Your feedback goes directly to the documentation team.