EmbedPDF

Measurements

On a floor plan or a technical drawing, people want real sizes: how long this wall is, how big this room. The measurement plugin adds the tools for it, and keeps each page’s scale, so a line of 3 centimeters on the page reads as 3 meters in the room. Register measurementPlugin() next to interactionPlugin() and annotationPlugin(), and turn on a tool:

tool.tsx
import { useInteraction } from '@embedpdf/react/interaction';

export function MeasureButton() {
  const interaction = useInteraction();

  return <button onClick={() => interaction.activateTool('distance')}>Measure</button>;
}
ToolMeasuresHow
distanceThe length of a lineDrag from one point to the other
perimeterThe length of a path of several linesClick each point, double-click to end
areaThe area of a shapeClick 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 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:

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 length

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

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

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

FieldWhat it isRead once
busyWhether a scale change is still runningisBusy()
calibrationRequestThe known length the user drew, { page, from, to }, waiting for its real length, or nullgetCalibrationRequest()
lastReportsThe reports of the last scale change, one per pagelistLastReports()

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.

SettingWhat it doesDefault
defaultScaleThe scale of a page that has none: 'metric', 'imperial', or a scale of your own'metric' (1:1 in meters)
presetsThe scales listPresets() offersthe eight above

A measurement’s look, such as its line, arrows and label position, is its tool’s defaults; see Tools & defaults.

Events#

events.tsx
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>;
}
EventFires whenCarries
onScaleChangedA page’s scale changedpage, report
onCalibrationRequestedThe user drew a known length, and your UI should ask for itrequest
onCalibrationCompletedA page was calibratedpage
onCalibrationDismissedCalibrating was cancelled—
onSettingsChangedThe settings changedsettings, changed

report lists the measurements recalculated for the new scale, and any that couldn’t be.

Permissions#

PermissionWhat it allowsCheck
doc.annotate.modifySetting a page’s scale and calibratingmeasurement.canCalibrate()
annotations:createMeasuringmeasurement.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.

Was this page helpful?

Your feedback goes directly to the documentation team.