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:
import { useInteraction } from '@embedpdf/react/interaction';
export function MeasureButton() {
const interaction = useInteraction();
return <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…
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
interactionPlugin,
useInteraction,
useInteractionState,
} from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import { measurementPlugin, useMeasurement, usePageScale } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';
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()) };
};
const TOOLS = [
{ id: 'pointer', label: 'Select' },
{ id: 'distance', label: 'Distance' },
{ id: 'perimeter', label: 'Perimeter' },
{ id: 'area', label: 'Area' },
];
function MeasureToolbar() {
const interaction = useInteraction();
const { activeToolId } = useInteractionState();
const measurement = useMeasurement();
const scale = usePageScale(0); // the cover's scale
const started = useRef(false);
// On load: the cover at 1:100, its width measured along the top, and the distance tool on.
useEffect(() => {
if (!scale.ready || started.current) return;
started.current = true;
void measurement
.setPreset(0, 'metric-100')
.then(() =>
measurement.createMeasurement({
kind: 'distance',
page: 0,
points: [
{ x: 30, y: 28 },
{ x: 582, y: 28 },
],
}),
)
.then(() => interaction.activateTool('distance'));
}, [measurement, interaction, scale.ready]);
return (
<div className="toolbar">
<div className="segmented" role="group" aria-label="Tool">
{TOOLS.map((tool) => (
<button
key={tool.id}
type="button"
aria-pressed={activeToolId === tool.id}
onClick={() => interaction.activateTool(tool.id)}
>
{tool.label}
</button>
))}
</div>
<span className="spacer" />
<output className="badge">
Scale {scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…'}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<MeasureToolbar />
<Stage className="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…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
measurementPlugin,
useMeasurement,
useMeasurementState,
usePageScale,
} from '@embedpdf/react/measurement';
import type { AreaUnit, LengthUnit } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// 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 };
// On load: a distance and an area on the cover, at 1:100, scrolled into view.
function MeasureCover() {
const measurement = useMeasurement();
const stage = useStage();
const ready = usePageScale(0).ready;
const started = useRef(false);
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void measurement.setPreset(0, 'metric-100').then(() =>
Promise.all([
measurement.createMeasurement({
kind: 'distance',
page: 0,
points: [
{ x: 60, y: 740 },
{ x: 550, y: 740 },
],
}),
measurement.createMeasurement({
kind: 'area',
page: 0,
points: [
{ x: 60, y: 580 },
{ x: 260, y: 580 },
{ x: 260, y: 700 },
{ x: 60, y: 700 },
],
}),
]).then(() => stage.reveal(0, { rect: LOWER_HALF })),
);
}, [measurement, stage, ready]);
return null;
}
function ScaleToolbar() {
const measurement = useMeasurement();
const { busy } = useMeasurementState();
const scale = usePageScale(0);
const [preset, setPreset] = useState('metric-100');
const [unit, setUnit] = useState<LengthUnit>('m');
const [areaUnit, setAreaUnit] = useState<AreaUnit>('m2');
const [precision, setPrecision] = useState(100);
// Every change recalculates the measurements already on the page.
return (
<div className="toolbar">
<label className="label">
Scale
<select
className="field"
value={preset}
disabled={busy}
onChange={(event) => {
setPreset(event.target.value);
void measurement.setPreset(0, event.target.value);
}}
>
{measurement.listPresets().map((choice) => (
<option key={choice.id} value={choice.id}>
{choice.label}
</option>
))}
</select>
</label>
<label className="label">
Length
<select
className="field"
value={unit}
disabled={busy}
onChange={(event) => {
const next = event.target.value as LengthUnit;
setUnit(next);
void measurement.setUnit(0, next);
}}
>
{measurement.listUnits().map((choice) => (
<option key={choice} value={choice}>
{choice}
</option>
))}
</select>
</label>
<label className="label">
Area
<select
className="field"
value={areaUnit}
disabled={busy}
onChange={(event) => {
const next = event.target.value as AreaUnit;
setAreaUnit(next);
void measurement.setAreaUnit(0, next);
}}
>
{measurement.listAreaUnits().map((choice) => (
<option key={choice} value={choice}>
{choice}
</option>
))}
</select>
</label>
<label className="label">
Steps
<select
className="field"
value={precision}
disabled={busy}
onChange={(event) => {
const next = Number(event.target.value);
setPrecision(next);
void measurement.setPrecision(0, next);
}}
>
{PRECISIONS.map((choice) => (
<option key={choice} value={choice}>
{choice === 1 ? 'Whole' : `1/${choice}`}
</option>
))}
</select>
</label>
<span className="spacer" />
<output className="badge">
{scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…'}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<MeasureCover />
<ScaleToolbar />
<Stage className="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:
import { useMeasurement, useMeasurementState } from '@embedpdf/react/measurement';
import { LengthPrompt } from './length-prompt';
export function CalibrationDialog() {
const measurement = useMeasurement();
const { calibrationRequest } = useMeasurementState();
if (!calibrationRequest) return null;
const apply = (value: number) =>
measurement.calibrate({ ...calibrationRequest, distance: { value, unit: 'cm' } });
return <LengthPrompt onSubmit={apply} onCancel={() => measurement.dismissCalibration()} />;
}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 { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin, useInteraction } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
measurementPlugin,
useMeasurement,
useMeasurementState,
usePageScale,
} from '@embedpdf/react/measurement';
import type { LengthUnit } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';
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()) };
};
function Calibration() {
const measurement = useMeasurement();
const interaction = useInteraction();
const { calibrationRequest, busy } = useMeasurementState();
const scale = usePageScale(0);
const [length, setLength] = useState('20');
const [unit, setUnit] = useState<LengthUnit>('cm');
const started = useRef(false);
// On load: calibrating, so the next line you drag is the known length.
useEffect(() => {
if (!scale.ready || started.current) return;
started.current = true;
if (measurement.canCalibrate()) measurement.startCalibration();
}, [measurement, scale.ready]);
// The plugin asks for the real length of the line you drew.
if (calibrationRequest) {
const value = Number(length);
return (
<form
className="toolbar"
onSubmit={(event) => {
event.preventDefault();
void measurement
.calibrate({ ...calibrationRequest, distance: { value, unit } })
.then(() => interaction.activateTool('distance'));
}}
>
<span className="readout">That line is</span>
<input
className="field length"
type="number"
min="0"
step="any"
aria-label="Its real length"
value={length}
onChange={(event) => setLength(event.target.value)}
/>
<select
className="field"
aria-label="Unit"
value={unit}
onChange={(event) => setUnit(event.target.value as LengthUnit)}
>
{measurement.listUnits().map((choice) => (
<option key={choice} value={choice}>
{choice}
</option>
))}
</select>
<button type="submit" className="button" disabled={busy || !(value > 0)}>
Set the scale
</button>
<button type="button" className="button" onClick={() => measurement.dismissCalibration()}>
Cancel
</button>
</form>
);
}
return (
<div className="toolbar">
<button
type="button"
className="button"
disabled={!measurement.canCalibrate()}
onClick={() => measurement.startCalibration()}
>
Calibrate
</button>
<output className="readout">
Drag along something you know the length of, then measure with the new scale
</output>
<span className="spacer" />
<output className="badge">
{scale.measure?.subtype === 'rectilinear' ? scale.measure.ratio : '…'}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<Calibration />
<Stage className="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#
import type { AnnotationRef } from '@embedpdf/react/annotation';
import { useMeasurementReadout } from '@embedpdf/react/measurement';
export function MeasurementLabel({ annotation }: { annotation: AnnotationRef }) {
const readout = useMeasurementReadout(annotation);
// { kind: 'distance', value: 3.42, label: '3.42 m' }
return <span>{'unavailable' in readout ? '—' : readout.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…
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
AnnotationLayer,
annotationKey,
annotationPlugin,
useAnnotation,
useAnnotationList,
} from '@embedpdf/react/annotation';
import type { Annotation } from '@embedpdf/react/annotation';
import {
measurementPlugin,
useMeasurement,
useMeasurementReadout,
usePageScale,
} from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// The cover's empty lower half, where the measurements go.
const LOWER_HALF = { x: 40, y: 560, width: 532, height: 210 };
// On load: the cover at 1:50, and one measurement of each kind on it, scrolled into view.
function MeasureCover() {
const measurement = useMeasurement();
const stage = useStage();
const ready = usePageScale(0).ready;
const started = useRef(false);
useEffect(() => {
if (!ready || started.current) return;
started.current = true;
void measurement.setPreset(0, 'metric-50').then(() =>
Promise.all([
measurement.createMeasurement({
kind: 'distance',
page: 0,
points: [
{ x: 60, y: 740 },
{ x: 550, y: 740 },
],
}),
measurement.createMeasurement({
kind: 'perimeter',
page: 0,
points: [
{ x: 60, y: 580 },
{ x: 160, y: 640 },
{ x: 260, y: 590 },
],
}),
measurement.createMeasurement({
kind: 'area',
page: 0,
points: [
{ x: 80, y: 650 },
{ x: 250, y: 650 },
{ x: 250, y: 710 },
{ x: 80, y: 710 },
],
}),
]).then(() => stage.reveal(0, { rect: LOWER_HALF })),
);
}, [measurement, stage, ready]);
return null;
}
// A measurement is a line, polyline or polygon with a dimension intent.
const isMeasurement = (annotation: Annotation) =>
'intent' in annotation && !!annotation.intent?.endsWith('-dimension');
function Readout({ annotation }: { annotation: Annotation }) {
const readout = useMeasurementReadout(annotation.ref);
const { selection } = useAnnotation();
return (
<li>
<button type="button" className="row" onClick={() => selection.set([annotation.ref])}>
<span className="kind">{'unavailable' in readout ? 'No scale' : readout.kind}</span>
<span className="value">{'unavailable' in readout ? '—' : readout.label}</span>
</button>
</li>
);
}
function Readouts() {
const measurements = useAnnotationList({ pages: [0] }).filter(isMeasurement);
return (
<ul className="readouts" aria-label="Measurements on the cover">
{measurements.map((annotation) => (
<Readout key={annotationKey(annotation.ref)} annotation={annotation} />
))}
</ul>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<MeasureCover />
<div className="viewer">
<Stage className="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…
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin, useAnnotationList } from '@embedpdf/react/annotation';
import { measurementPlugin, useMeasurement, usePageScale } from '@embedpdf/react/measurement';
import type { MeasurementKind } from '@embedpdf/react/measurement';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// 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 };
function MeasureButtons() {
const measurement = useMeasurement();
const stage = useStage();
const scale = usePageScale(0);
const count = useAnnotationList({ pages: [0] }).filter(
(annotation) => 'intent' in annotation && !!annotation.intent?.endsWith('-dimension'),
).length;
const started = useRef(false);
// The same measurement the tool makes, with the page's scale and the tool's style.
const measure = (kind: MeasurementKind) =>
void measurement.createMeasurement({ kind, page: 0, points: SHAPES[kind] });
// On load: an area, scrolled into view.
useEffect(() => {
if (!scale.ready || started.current) return;
started.current = true;
void measurement
.createMeasurement({ kind: 'area', page: 0, points: SHAPES.area })
.then(() => stage.reveal(0, { rect: LOWER_HALF }));
}, [measurement, stage, scale.ready]);
return (
<div className="toolbar">
<button
type="button"
className="button"
disabled={!scale.ready}
onClick={() => measure('distance')}
>
Distance
</button>
<button
type="button"
className="button"
disabled={!scale.ready}
onClick={() => measure('perimeter')}
>
Perimeter
</button>
<button
type="button"
className="button"
disabled={!scale.ready}
onClick={() => measure('area')}
>
Area
</button>
<span className="spacer" />
<output className="readout">
{count} {count === 1 ? 'measurement' : 'measurements'} on the cover
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<MeasureButtons />
<Stage className="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. Your component re-renders when a value changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
busy | Whether a scale change is still running | isBusy() |
calibrationRequest | The known length the user drew, { page, from, to }, waiting for its real length, or null | getCalibrationRequest() |
lastReports | The reports of the last scale change, one per page | listLastReports() |
A page’s scale and a measurement’s readout come from usePageScale(page) and useMeasurementReadout(ref).
Keeping them apart means a component re-renders only for its own page or measurement.
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#
import { useState } from 'react';
import { useMeasurementEvent } from '@embedpdf/react/measurement';
export function ScaleNotice() {
const [message, setMessage] = useState('');
useMeasurementEvent(
(measurement) => measurement.onScaleChanged,
() => setMessage('The scale changed'),
);
return <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.