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.svelte
<script lang="ts">
  import { useInteraction } from '@embedpdf/svelte/interaction';

  const interaction = useInteraction();
</script>

<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…

<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 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…

<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 length

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

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

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

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

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