Message APINPM Package/docs/components/measurement/calibration

Calibration

Calculate document scale from a reference line and a known real-world length using the SDK.

Overview

Use viewer.measurements.calibration when the operator needs to calculate a document scale from a picked reference line and a known real-world length.

Calibration commands are available after the viewer is mounted, Canvas is ready, and a document is open.

typescript
import { createViewer } from "@rasterex/viewer";

const viewer = createViewer({
  container: "#viewer"
});

await viewer.mount();
await viewer.ready();

await viewer.documents.open({
  url: "https://files.example.com/floor-plan.pdf",
  displayName: "Floor Plan.pdf"
});
Overview

Calibration Flow

Calibration is a guided flow:

1. Start calibration mode.

2. Let the user pick the reference line in Canvas.

3. Wait for finished event.

4. Calculate a candidate scale from the known real-world length.

5. Apply the calculated scale.

Start Calibration

Start the calibration mode for a specific file index.

  • start(...) returns the generated request ID. Keep it in your UI state and reuse it for calculate(...), apply(...), or cancel(...).
  • Canvas emits finished after the user picks the reference line in Canvas:

Subscribe to calibration finished

const unsubscribe = viewer.measurements.calibration.on("finished", (event) => {
  console.log("Request ID:", event.requestId);
  console.log("Measured length from Canvas:", event.measuredLength);
});

// To clean up the listener later:
unsubscribe();
typescript
const requestId = viewer.measurements.calibration.start({
  fileIndex: 0
});
Start Calibration

Calculate Metric Scale

Calculate a candidate scale using metric units. Use measurementSystem: 1 for metric systems.

typescript
const candidate = await viewer.measurements.calibration.calculate({
  requestId,
  fileIndex: 0,
  measurementSystem: 1,
  metricUnit: "Meter",
  calibrateCorrectionMetricValue: 10,
  dimPrecision: 2,
  pageRanges: [[1, 5]],
  totalPages: 5,
  timeoutMs: 10000
});

console.log(candidate.scale);
Calculate Metric Scale

Calculate Imperial Scale

Calculate a candidate scale using imperial units. Use measurementSystem: 2 for imperial systems. Provide feet, inches, or both. For imperial precision, pass the fractional denominator, for example 32 for 0 ft 0 1/32 in.

typescript
const candidate = await viewer.measurements.calibration.calculate({
  requestId,
  fileIndex: 0,
  measurementSystem: 2,
  calibrateCorrectionFeetValue: 20,
  calibrateCorrectionInchValue: 7,
  dimPrecision: 32,
  timeoutMs: 10000
});

console.log(candidate.scale.label);
Calculate Imperial Scale

Apply The Calculated Scale

Apply the calculated scale candidate to the Canvas. This sends the Canvas apply command and waits for the next matching scales snapshot.

typescript
const snapshot = await viewer.measurements.calibration.apply({
  requestId,
  fileIndex: 0,
  timeoutMs: 10000
});

console.log(snapshot.selectedLabel);
Apply The Calculated Scale

Cancel Calibration

Aborts the active calibration flow. Call this if the user cancels or closes the calibration modal/panel.

typescript
viewer.measurements.calibration.cancel({
  requestId,
  fileIndex: 0
});
Cancel Calibration

Event Subscriptions

Listen for calibration events natively:

typescript
const offFinished = viewer.measurements.calibration.on("finished", (event) => {
  if (event.isFinished) {
    console.log("Reference line picked", event.measuredLength);
  }
});

const offCalculated = viewer.measurements.calibration.on("scaleCalculated", (event) => {
  console.log("Candidate scale calculated", event.scale.label);
});
Event Subscriptions

Types Reference

Important types exported by the library for calibration workflows:

typescript
type CalibrationFinishedEvent = {
  requestId?: string;
  fileIndex?: number;
  fileName?: string;
  isFinished: boolean;
  measuredLength?: string | number;
  [key: string]: unknown;
};

type CalibrationScaleCalculatedEvent = {
  requestId?: string;
  fileIndex?: number;
  fileName?: string;
  scale: MeasurementScale;
  [key: string]: unknown;
};

type CalibrationMetricSetOptions = {
  requestId?: string;
  fileIndex?: number;
  measurementSystem: 1;
  metricUnit: "Millimeter" | "Centimeter" | "Decimeter" | "Meter" | "Kilometer";
  calibrateCorrectionMetricValue: number | string;
  dimPrecision: number;
  pageRanges?: [number, number][];
  totalPages?: number;
  timeoutMs?: number;
};

type CalibrationImperialSetOptions = {
  requestId?: string;
  fileIndex?: number;
  measurementSystem: 2;
  calibrateCorrectionFeetValue?: number | string;
  calibrateCorrectionInchValue?: number | string;
  dimPrecision: number;
  pageRanges?: [number, number][];
  totalPages?: number;
  timeoutMs?: number;
};
Types Reference

Validation and Errors

The SDK validates calibration inputs before sending requests to Canvas. A request will fail if:

  • dimPrecision is not a non-negative integer.
  • totalPages (when provided) is not a positive integer.
  • pageRanges are not 1-based inclusive [start, end] pairs.
  • Metric calibration does not specify a supported unit system or value.
  • Imperial calibration does not specify feet or inches values greater than zero.

Vanilla Integration

Complete implementation code setup for Vanilla HTML/JS environments:

<div id="viewer" style="height: 640px"></div>

<div>
  <button type="button" id="start-calibration">Start Calibration</button>
  <input id="known-length" type="number" min="0" step="0.01" value="10" />
  <select id="metric-unit">
    <option value="Meter">Meter</option>
    <option value="Millimeter">Millimeter</option>
    <option value="Centimeter">Centimeter</option>
  </select>
  <button type="button" id="calculate-scale">Calculate</button>
  <button type="button" id="apply-scale">Apply</button>
  <button type="button" id="cancel-calibration">Cancel</button>
</div>

<p id="status"></p>
Vanilla Integration

React Integration

Complete React Component implementation details:

tsx
import { useEffect, useRef, useState } from "react";
import { createViewer, type RasterexViewer } from "@rasterex/viewer";

export function CalibrationPanel() {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const viewerRef = useRef<RasterexViewer | null>(null);
  const [requestId, setRequestId] = useState<string | null>(null);
  const [picked, setPicked] = useState(false);
  const [candidateReady, setCandidateReady] = useState(false);
  const [knownLength, setKnownLength] = useState("10");
  const [status, setStatus] = useState("Loading viewer");

  useEffect(() => {
    let disposed = false;
    const viewer = createViewer({ container: hostRef.current! });
    viewerRef.current = viewer;

    const offFinished = viewer.measurements.calibration.on("finished", (event) => {
      if (disposed) return;
      setRequestId((current) => event.requestId ?? current);
      setPicked(true);
      setStatus(`Reference picked: ${event.measuredLength ?? "ready"}`);
    });

    const offCalculated = viewer.measurements.calibration.on("scaleCalculated", (event) => {
      if (disposed) return;
      setCandidateReady(true);
      setStatus(`Candidate scale: ${event.scale.label}`);
    });

    async function startViewer() {
      await viewer.mount();
      await viewer.ready();
      await viewer.documents.open({
        url: "https://files.example.com/floor-plan.pdf",
        displayName: "Floor Plan.pdf"
      });
      if (!disposed) setStatus("Ready");
    }

    void startViewer().catch((error) => {
      if (!disposed) setStatus(error instanceof Error ? error.message : "Viewer failed");
    });

    return () => {
      disposed = true;
      offFinished();
      offCalculated();
      viewer.destroy();
      viewerRef.current = null;
    };
  }, []);

  function startCalibration() {
    const id = viewerRef.current?.measurements.calibration.start({ fileIndex: 0 });
    if (!id) return;
    setRequestId(id);
    setPicked(false);
    setCandidateReady(false);
    setStatus("Pick a reference line in the viewer");
  }

  async function calculateScale() {
    if (!requestId || !picked) {
      setStatus("Pick a reference line first");
      return;
    }

    try {
      const event = await viewerRef.current?.measurements.calibration.calculate({
        requestId,
        fileIndex: 0,
        measurementSystem: 1,
        metricUnit: "Meter",
        calibrateCorrectionMetricValue: knownLength,
        dimPrecision: 2,
        timeoutMs: 10000
      });
      setCandidateReady(true);
      setStatus(`Candidate scale: ${event?.scale.label ?? "calculated"}`);
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Calculate failed");
    }
  }

  async function applyScale() {
    if (!requestId || !candidateReady) {
      setStatus("Calculate a scale first");
      return;
    }

    try {
      const snapshot = await viewerRef.current?.measurements.calibration.apply({
        requestId,
        fileIndex: 0,
        timeoutMs: 10000
      });
      setStatus(`Applied: ${snapshot?.selectedLabel ?? "scale"}`);
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Apply failed");
    }
  }

  function cancelCalibration() {
    viewerRef.current?.measurements.calibration.cancel({
      requestId: requestId ?? undefined,
      fileIndex: 0
    });
    setRequestId(null);
    setPicked(false);
    setCandidateReady(false);
    setStatus("Calibration canceled");
  }

  return (
    <section>
      <div ref={hostRef} style={{ height: 640 }} />
      <button type="button" onClick={startCalibration}>
        Start Calibration
      </button>
      <input
        type="number"
        min="0"
        step="0.01"
        value={knownLength}
        onChange={(event) => setKnownLength(event.currentTarget.value)}
      />
      <button type="button" onClick={() => void calculateScale()}>
        Calculate
      </button>
      <button type="button" onClick={() => void applyScale()}>
        Apply
      </button>
      <button type="button" onClick={cancelCalibration}>
        Cancel
      </button>
      <p>{status}</p>
    </section>
  );
}
React Integration

Next.js Integration

Calibration triggers user interface loops and requires React client mode context:

tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { createViewer, type RasterexViewer } from "@rasterex/viewer";

export default function CalibrationPage() {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const viewerRef = useRef<RasterexViewer | null>(null);
  const [requestId, setRequestId] = useState<string | null>(null);
  const [status, setStatus] = useState("Loading viewer");

  useEffect(() => {
    let disposed = false;
    const viewer = createViewer({ container: hostRef.current! });
    viewerRef.current = viewer;

    const offFinished = viewer.measurements.calibration.on("finished", (event) => {
      if (disposed) return;
      setRequestId((current) => event.requestId ?? current);
      setStatus(`Reference picked: ${event.measuredLength ?? "ready"}`);
    });

    async function startViewer() {
      await viewer.mount();
      await viewer.ready();
      await viewer.documents.open({
        url: "https://files.example.com/floor-plan.pdf",
        displayName: "Floor Plan.pdf"
      });
      if (!disposed) setStatus("Ready");
    }

    void startViewer().catch((error) => {
      if (!disposed) setStatus(error instanceof Error ? error.message : "Viewer failed");
    });

    return () => {
      disposed = true;
      offFinished();
      viewer.destroy();
      viewerRef.current = null;
    };
  }, []);

  function startCalibration() {
    const id = viewerRef.current?.measurements.calibration.start({ fileIndex: 0 });
    if (id) {
      setRequestId(id);
      setStatus("Pick a reference line in the viewer");
    }
  }

  async function calculateAndApply() {
    if (!requestId) {
      setStatus("Start calibration first");
      return;
    }

    try {
      const candidate = await viewerRef.current?.measurements.calibration.calculate({
        requestId,
        fileIndex: 0,
        measurementSystem: 1,
        metricUnit: "Meter",
        calibrateCorrectionMetricValue: 10,
        dimPrecision: 2,
        timeoutMs: 10000
      });
      setStatus(`Candidate scale: ${candidate?.scale.label ?? "calculated"}`);

      const snapshot = await viewerRef.current?.measurements.calibration.apply({
        requestId,
        fileIndex: 0,
        timeoutMs: 10000
      });
      setStatus(`Applied: ${snapshot?.selectedLabel ?? "scale"}`);
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Calibration failed");
    }
  }

  return (
    <main>
      <div ref={hostRef} style={{ height: 640 }} />
      <button type="button" onClick={startCalibration}>
        Start Calibration
      </button>
      <button type="button" onClick={() => void calculateAndApply()}>
        Calculate And Apply
      </button>
      <p>{status}</p>
    </main>
  );
}
Next.js Integration

Angular Integration

Complete integration model details for Angular components:

typescript
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  ViewChild
} from "@angular/core";
import { createViewer, type RasterexViewer } from "@rasterex/viewer";

@Component({
  selector: "app-measurement-calibration",
  template: `
    <div #viewerHost class="viewer-host"></div>
    <button type="button" (click)="startCalibration()">Start Calibration</button>
    <input type="number" min="0" step="0.01" [(ngModel)]="knownLength" />
    <button type="button" (click)="calculateScale()">Calculate</button>
    <button type="button" (click)="applyScale()">Apply</button>
    <button type="button" (click)="cancelCalibration()">Cancel</button>
    <p>{{ status }}</p>
  `,
  styles: [".viewer-host { height: 640px; }"]
})
export class MeasurementCalibrationComponent implements AfterViewInit, OnDestroy {
  @ViewChild("viewerHost", { static: true })
  private viewerHost!: ElementRef<HTMLDivElement>;

  protected knownLength = "10";
  protected status = "Loading viewer";
  private viewer: RasterexViewer | null = null;
  private requestId: string | null = null;
  private picked = false;
  private candidateReady = false;
  private cleanups: Array<() => void> = [];

  async ngAfterViewInit() {
    const viewer = createViewer({
      container: this.viewerHost.nativeElement
    });
    this.viewer = viewer;

    this.cleanups.push(
      viewer.measurements.calibration.on("finished", (event) => {
        this.requestId = event.requestId ?? this.requestId;
        this.picked = true;
        this.status = `Reference picked: ${event.measuredLength ?? "ready"}`;
      }),
      viewer.measurements.calibration.on("scaleCalculated", (event) => {
        this.candidateReady = true;
        this.status = `Candidate scale: ${event.scale.label}`;
      })
    );

    try {
      await viewer.mount();
      await viewer.ready();
      await viewer.documents.open({
        url: "https://files.example.com/floor-plan.pdf",
        displayName: "Floor Plan.pdf"
      });
      this.status = "Ready";
    } catch (error) {
      this.status = error instanceof Error ? error.message : "Viewer failed";
    }
  }

  startCalibration() {
    this.requestId = this.viewer?.measurements.calibration.start({ fileIndex: 0 }) ?? null;
    this.picked = false;
    this.candidateReady = false;
    this.status = "Pick a reference line in the viewer";
  }

  async calculateScale() {
    if (!this.requestId || !this.picked) {
      this.status = "Pick a reference line first";
      return;
    }

    try {
      const event = await this.viewer?.measurements.calibration.calculate({
        requestId: this.requestId,
        fileIndex: 0,
        measurementSystem: 1,
        metricUnit: "Meter",
        calibrateCorrectionMetricValue: this.knownLength,
        dimPrecision: 2,
        timeoutMs: 10000
      });
      this.candidateReady = true;
      this.status = `Candidate scale: ${event?.scale.label ?? "calculated"}`;
    } catch (error) {
      this.status = error instanceof Error ? error.message : "Calculate failed";
    }
  }

  async applyScale() {
    if (!this.requestId || !this.candidateReady) {
      this.status = "Calculate a scale first";
      return;
    }

    try {
      const snapshot = await this.viewer?.measurements.calibration.apply({
        requestId: this.requestId,
        fileIndex: 0,
        timeoutMs: 10000
      });
      this.status = `Applied: ${snapshot?.selectedLabel ?? "scale"}`;
    } catch (error) {
      this.status = error instanceof Error ? error.message : "Apply failed";
    }
  }

  cancelCalibration() {
    this.viewer?.measurements.calibration.cancel({
      requestId: this.requestId ?? undefined,
      fileIndex: 0
    });
    this.requestId = null;
    this.picked = false;
    this.candidateReady = false;
    this.status = "Calibration canceled";
  }

  ngOnDestroy() {
    for (const cleanup of this.cleanups) cleanup();
    this.viewer?.destroy();
  }
}
Angular Integration

Canvas Broker Mapping

Calibration actions map cleanly to Canvas broker command flows:

  • viewer.measurements.calibration.start(...) sends startCalibrationV2.
  • viewer.measurements.calibration.on("finished", ...) listens for calibrationFinished.
  • viewer.measurements.calibration.calculate(...) sends setCalibrationV2 and awaits calibrationScaleCalculated.
  • viewer.measurements.calibration.on("scaleCalculated", ...) listens for calibrationScaleCalculated.
  • viewer.measurements.calibration.apply(...) sends addCalibrationScaleV2 and awaits scalesSnapshot.
  • viewer.measurements.calibration.cancel(...) sends cancelCalibration.